Index: ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 21474)
@@ -1,446 +1,457 @@
/**
* Highlights the victory condition in the game-description.
*/
var g_DescriptionHighlight = "orange";
/**
* The rating assigned to lobby players who didn't complete a ranked 1v1 yet.
*/
var g_DefaultLobbyRating = 1200;
/**
* XEP-0172 doesn't restrict nicknames, but our lobby policy does.
* So use this human readable delimiter to separate buddy names in the config file.
*/
var g_BuddyListDelimiter = ",";
/**
* Returns the nickname without the lobby rating.
*/
function splitRatingFromNick(playerName)
{
let result = /^(\S+)\ \((\d+)\)$/g.exec(playerName);
return { "nick": result ? result[1] : playerName, "rating": result ? +result[2] : "" };
}
/**
* Array of playernames that the current user has marked as buddies.
*/
var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyListDelimiter);
/**
* Denotes which players are a lobby buddy of the current user.
*/
var g_BuddySymbol = '•';
/**
* Returns map description and preview image or placeholder.
*/
function getMapDescriptionAndPreview(mapType, mapName)
{
let mapData;
if (mapType == "random" && mapName == "random")
mapData = { "settings": { "Description": translate("A randomly selected map.") } };
else if (mapType == "random" && Engine.FileExists(mapName + ".json"))
mapData = Engine.ReadJSONFile(mapName + ".json");
else if (Engine.FileExists(mapName + ".xml"))
mapData = Engine.LoadMapSettings(mapName + ".xml");
return deepfreeze({
"description": mapData && mapData.settings && mapData.settings.Description ? translate(mapData.settings.Description) : translate("Sorry, no description available."),
"preview": mapData && mapData.settings && mapData.settings.Preview ? mapData.settings.Preview : "nopreview.png"
});
}
/**
* Sets the mappreview image correctly.
* It needs to be cropped as the engine only allows loading square textures.
*
* @param {string} guiObject
* @param {string} filename
*/
function setMapPreviewImage(guiObject, filename)
{
Engine.GetGUIObjectByName(guiObject).sprite =
"cropped:" + 400 / 512 + "," + 300 / 512 + ":" +
"session/icons/mappreview/" + filename;
}
/**
* Returns a formatted string describing the player assignments.
* Needs g_CivData to translate!
*
* @param {object} playerDataArray - As known from gamesetup and simstate.
* @param {(string[]|false)} playerStates - One of "won", "defeated", "active" for each player.
* @returns {string}
*/
function formatPlayerInfo(playerDataArray, playerStates)
{
let playerDescriptions = {};
let playerIdx = 0;
for (let playerData of playerDataArray)
{
if (playerData == null || playerData.Civ && playerData.Civ == "gaia")
continue;
++playerIdx;
let teamIdx = playerData.Team;
let isAI = playerData.AI && playerData.AI != "";
let playerState = playerStates && playerStates[playerIdx] || playerData.State;
let isActive = !playerState || playerState == "active";
let playerDescription;
if (isAI)
{
if (playerData.Civ)
{
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(AIdescription)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(AIdescription)s, %(state)s)");
}
else
{
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(AIdescription)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(AIdescription)s, %(state)s)");
}
}
else
{
if (playerData.Offline)
{
// Can only occur in the lobby for now, so no strings with civ needed
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (OFFLINE)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (OFFLINE, %(state)s)");
}
else
{
if (playerData.Civ)
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(state)s)");
else
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(state)s)");
}
}
// Sort player descriptions by team
if (!playerDescriptions[teamIdx])
playerDescriptions[teamIdx] = [];
let playerNick = splitRatingFromNick(playerData.Name).nick;
playerDescriptions[teamIdx].push(sprintf(playerDescription, {
"playerName":
coloredText(
(g_Buddies.indexOf(playerNick) != -1 ? g_BuddySymbol + " " : "") +
escapeText(playerData.Name),
(typeof getPlayerColor == 'function' ?
(isAI ? "white" : getPlayerColor(playerNick)) :
rgbToGuiColor(playerData.Color || g_Settings.PlayerDefaults[playerIdx].Color))),
"civ":
!playerData.Civ ?
translate("Unknown Civilization") :
g_CivData && g_CivData[playerData.Civ] && g_CivData[playerData.Civ].Name ?
translate(g_CivData[playerData.Civ].Name) :
playerData.Civ,
"state":
playerState == "defeated" ?
translateWithContext("playerstate", "defeated") :
translateWithContext("playerstate", "won"),
"AIdescription": translateAISettings(playerData)
}));
}
let teams = Object.keys(playerDescriptions);
if (teams.indexOf("observer") > -1)
teams.splice(teams.indexOf("observer"), 1);
let teamDescription = [];
// If there are no teams, merge all playersDescriptions
if (teams.length == 1)
teamDescription.push(playerDescriptions[teams[0]].join("\n"));
// If there are teams, merge "Team N:" + playerDescriptions
else
teamDescription = teams.map(team => {
let teamCaption = team == -1 ?
translate("No Team") :
sprintf(translate("Team %(team)s"), { "team": +team + 1 });
// Translation: Describe players of one team in a selected game, f.e. in the replay- or savegame menu or lobby
return sprintf(translate("%(team)s:\n%(playerDescriptions)s"), {
"team": '[font="sans-bold-14"]' + teamCaption + "[/font]",
"playerDescriptions": playerDescriptions[team].join("\n")
});
});
if (playerDescriptions.observer)
teamDescription.push(sprintf(translate("%(team)s:\n%(playerDescriptions)s"), {
"team": '[font="sans-bold-14"]' + translatePlural("Observer", "Observers", playerDescriptions.observer.length) + "[/font]",
"playerDescriptions": playerDescriptions.observer.join("\n")
}));
return teamDescription.join("\n\n");
}
/**
* Sets an additional map label, map preview image and describes the chosen gamesettings more closely.
*
* Requires g_GameAttributes and g_VictoryConditions.
*/
function getGameDescription(extended = false)
{
let titles = [];
+ if (!g_GameAttributes.settings.VictoryConditions.length)
+ titles.push({
+ "label": translateWithContext("victory condition", "Endless Game"),
+ "value": translate("No winner will be determined, even if everyone is defeated.")
+ });
+
+ let victoryConditions = g_GameAttributes.settings.VictoryConditions.map(victoryConditionName =>
+ g_VictoryConditions.find(victoryCondition => victoryCondition.Name == victoryConditionName) || {
+ "Name": victoryConditionName,
+ "Description": ""
+ });
- let victoryIdx = g_VictoryConditions.Name.indexOf(g_GameAttributes.settings.GameType || g_VictoryConditions.Default);
- if (victoryIdx != -1)
+ for (let victoryCondition of victoryConditions.sort((a, b) =>
+ a.GUIOrder - b.GUIOrder || (a.Title > b.Title ? 1 : a.Title > b.Title ? -1 : 0)))
{
- let title = g_VictoryConditions.Title[victoryIdx];
- if (g_VictoryConditions.Name[victoryIdx] == "wonder")
+ let title = translateVictoryCondition(victoryCondition.Name);
+ if (victoryCondition.Name == "wonder")
title = sprintf(
translatePluralWithContext(
"victory condition",
"Wonder (%(min)s minute)",
"Wonder (%(min)s minutes)",
g_GameAttributes.settings.WonderDuration
),
{ "min": g_GameAttributes.settings.WonderDuration }
);
- let isCaptureTheRelic = g_VictoryConditions.Name[victoryIdx] == "capture_the_relic";
+ let isCaptureTheRelic = victoryCondition.Name == "capture_the_relic";
if (isCaptureTheRelic)
title = sprintf(
translatePluralWithContext(
"victory condition",
"Capture the Relic (%(min)s minute)",
"Capture the Relic (%(min)s minutes)",
g_GameAttributes.settings.RelicDuration
),
{ "min": g_GameAttributes.settings.RelicDuration }
);
titles.push({
"label": title,
- "value": g_VictoryConditions.Description[victoryIdx]
+ "value": victoryCondition.Description
});
if (isCaptureTheRelic)
titles.push({
"label": translate("Relic Count"),
"value": g_GameAttributes.settings.RelicCount
});
- if (g_VictoryConditions.Name[victoryIdx] == "regicide")
+ if (victoryCondition.Name == "regicide")
if (g_GameAttributes.settings.RegicideGarrison)
titles.push({
"label": translate("Hero Garrison"),
"value": translate("Heroes can be garrisoned.")
});
else
titles.push({
"label": translate("Exposed Heroes"),
- "value": translate("Heroes cannot be garrisoned, and they are vulnerable to raids.")
+ "value": translate("Heroes cannot be garrisoned and they are vulnerable to raids.")
});
}
if (g_GameAttributes.settings.RatingEnabled &&
g_GameAttributes.settings.PlayerData.length == 2)
titles.push({
"label": translate("Rated game"),
"value": translate("When the winner of this match is determined, the lobby score will be adapted.")
});
if (g_GameAttributes.settings.LockTeams)
titles.push({
"label": translate("Locked Teams"),
"value": translate("Players can't change the initial teams.")
});
else
titles.push({
"label": translate("Diplomacy"),
"value": translate("Players can make alliances and declare war on allies.")
});
if (g_GameAttributes.settings.LastManStanding)
titles.push({
"label": translate("Last Man Standing"),
"value": translate("Only one player can win the game. If the remaining players are allies, the game continues until only one remains.")
});
else
titles.push({
"label": translate("Allied Victory"),
"value": translate("If one player wins, his or her allies win too. If one group of allies remains, they win.")
});
if (extended)
{
titles.push({
"label": translate("Ceasefire"),
"value":
g_GameAttributes.settings.Ceasefire == 0 ?
translate("disabled") :
sprintf(translatePlural(
"For the first minute, other players will stay neutral.",
"For the first %(min)s minutes, other players will stay neutral.",
g_GameAttributes.settings.Ceasefire),
{ "min": g_GameAttributes.settings.Ceasefire })
});
titles.push({
"label": translate("Map Name"),
"value": translate(g_GameAttributes.settings.Name)
});
titles.push({
"label": translate("Map Type"),
"value": g_MapTypes.Title[g_MapTypes.Name.indexOf(g_GameAttributes.mapType)]
});
if (g_GameAttributes.mapType == "random")
{
let mapSize = g_MapSizes.Name[g_MapSizes.Tiles.indexOf(g_GameAttributes.settings.Size)];
if (mapSize)
titles.push({
"label": translate("Map Size"),
"value": mapSize
});
}
}
titles.push({
"label": translate("Map Description"),
"value":
g_GameAttributes.map == "random" ?
translate("Randomly selects a map from the list") :
g_GameAttributes.settings.Description ?
translate(g_GameAttributes.settings.Description) :
translate("Sorry, no description available.")
});
if (extended || g_GameAttributes.settings.Nomad)
titles.push({
"label": g_GameAttributes.settings.Nomad ? translate("Nomad Mode") : translate("Civic Centers"),
"value":
g_GameAttributes.settings.Nomad ?
translate("Players start with only few units and have to find a suitable place to build their city.") :
translate("Players start with a Civic Center.")
});
if (g_GameAttributes.settings.Biome)
{
let biome = g_Settings.Biomes.find(b => b.Id == g_GameAttributes.settings.Biome);
titles.push({
"label": translate("Biome"),
"value": biome ? biome.Title : translateWithContext("biome", "Random")
});
}
if (extended)
{
titles.push({
"label": translate("Starting Resources"),
"value": sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), {
"startingResourcesTitle":
g_StartingResources.Title[
g_StartingResources.Resources.indexOf(
g_GameAttributes.settings.StartingResources)],
"amount": g_GameAttributes.settings.StartingResources
})
});
titles.push({
"label": translate("Population Limit"),
"value":
g_PopulationCapacities.Title[
g_PopulationCapacities.Population.indexOf(
g_GameAttributes.settings.PopulationCap)]
});
titles.push({
"label": translate("Treasures"),
"value": g_GameAttributes.settings.DisableTreasures ?
translateWithContext("treasures", "Disabled") :
translateWithContext("treasures", "As defined by the map.")
});
titles.push({
"label": translate("Revealed Map"),
"value": g_GameAttributes.settings.RevealMap
});
titles.push({
"label": translate("Explored Map"),
"value": g_GameAttributes.settings.ExploreMap
});
titles.push({
"label": translate("Cheats"),
"value": g_GameAttributes.settings.CheatsEnabled
});
}
return titles.map(title => sprintf(translate("%(label)s %(details)s"), {
"label": coloredText(title.label, g_DescriptionHighlight),
"details":
title.value === true ? translateWithContext("gamesetup option", "enabled") :
title.value || translateWithContext("gamesetup option", "disabled")
})).join("\n");
}
/**
* Sets the win/defeat icon to indicate current player's state.
* @param {string} state - The current in-game state of the player.
* @param {string} imageID - The name of the XML image object to update.
*/
function setOutcomeIcon(state, imageID)
{
let image = Engine.GetGUIObjectByName(imageID);
if (state == "won")
{
image.sprite = "stretched:session/icons/victory.png";
image.tooltip = translate("Victorious");
}
else if (state == "defeated")
{
image.sprite = "stretched:session/icons/defeat.png";
image.tooltip = translate("Defeated");
}
}
function translateAISettings(playerData)
{
if (!playerData.AI)
return "";
return sprintf(translate("%(AIdifficulty)s %(AIbehavior)s %(AIname)s"), {
"AIname": translateAIName(playerData.AI),
"AIdifficulty": translateAIDifficulty(playerData.AIDiff),
"AIbehavior": translateAIBehavior(playerData.AIBehavior),
});
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/capture_the_relic.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/capture_the_relic.json (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/capture_the_relic.json (revision 21474)
@@ -1,15 +1,14 @@
{
"TranslatedKeys": ["Title", "Description"],
"Data":
{
"Title": "Capture the Relic",
"Description": "Capture all relics spread across the map and keep them for a certain time to win the game.",
"Scripts":
[
"scripts/TriggerHelper.js",
- "scripts/ConquestCommon.js",
- "scripts/Conquest.js",
"scripts/CaptureTheRelic.js"
- ]
+ ],
+ "GUIOrder": 10
}
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/conquest_structures.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/conquest_structures.json (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/conquest_structures.json (revision 21474)
@@ -1,14 +1,16 @@
{
"TranslatedKeys": ["Title", "Description"],
"Data":
{
"Title": "Conquest Structures",
- "Description": "Destroy all enemy structures to win.",
+ "Description": "Defeat opponents by destroying all their structures.",
"Scripts":
[
"scripts/TriggerHelper.js",
"scripts/ConquestCommon.js",
"scripts/ConquestStructures.js"
- ]
+ ],
+ "DisabledWhenChecked": ["conquest"],
+ "GUIOrder": 1
}
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/regicide.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/regicide.json (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/regicide.json (revision 21474)
@@ -1,15 +1,14 @@
{
"TranslatedKeys": ["Title", "Description"],
"Data":
{
"Title": "Regicide",
"Description": "Defeat opponents by killing their hero.",
"Scripts":
[
"scripts/TriggerHelper.js",
- "scripts/ConquestCommon.js",
- "scripts/Conquest.js",
"scripts/Regicide.js"
- ]
+ ],
+ "GUIOrder": 10
}
}
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js (revision 21474)
@@ -1,87 +1,87 @@
/**
* Used to initialize non-player settings relevant to the map, like
* default stance and victory conditions. DO NOT load players here
*/
function LoadMapSettings(settings)
{
if (!settings)
settings = {};
if (settings.DefaultStance)
{
for (let ent of Engine.GetEntitiesWithInterface(IID_UnitAI))
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SwitchToStance(settings.DefaultStance);
}
}
if (settings.RevealMap)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
cmpRangeManager.SetLosRevealAll(-1, true);
}
if (settings.DisableTreasures)
for (let ent of Engine.GetEntitiesWithInterface(IID_ResourceSupply))
{
let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply.GetType().generic == "treasure")
Engine.DestroyEntity(ent);
}
if (settings.CircularMap)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
cmpRangeManager.SetLosCircular(true);
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
if (cmpObstructionManager)
cmpObstructionManager.SetPassabilityCircular(true);
}
if (settings.TriggerDifficulty != undefined)
Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).SetDifficulty(settings.TriggerDifficulty);
else if (settings.SupportedTriggerDifficulties) // used by Atlas and autostart games
{
let difficulties = Engine.ReadJSONFile("simulation/data/settings/trigger_difficulties.json").Data;
let defaultDiff = difficulties.find(d => d.Name == settings.SupportedTriggerDifficulties.Default).Difficulty;
Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).SetDifficulty(defaultDiff);
}
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
- let gameTypeSettings = {};
- if (settings.GameType && settings.GameType == "capture_the_relic")
- gameTypeSettings.relicCount = settings.RelicCount;
- if (settings.GameType && settings.GameType == "capture_the_relic")
- gameTypeSettings.relicDuration = settings.RelicDuration * 60 * 1000;
- if (settings.GameType && settings.GameType == "wonder")
- gameTypeSettings.wonderDuration = settings.WonderDuration * 60 * 1000;
- if (settings.GameType && settings.GameType == "regicide")
- gameTypeSettings.regicideGarrison = settings.RegicideGarrison;
- if (settings.GameType)
- cmpEndGameManager.SetGameType(settings.GameType, gameTypeSettings);
+ let gameSettings = { "victoryConditions": settings.VictoryConditions };
+ if (gameSettings.victoryConditions.indexOf("capture_the_relic") != -1)
+ {
+ gameSettings.relicCount = settings.RelicCount;
+ gameSettings.relicDuration = settings.RelicDuration * 60 * 1000;
+ }
+ if (gameSettings.victoryConditions.indexOf("wonder") != -1)
+ gameSettings.wonderDuration = settings.WonderDuration * 60 * 1000;
+ if (gameSettings.victoryConditions.indexOf("regicide") != -1)
+ gameSettings.regicideGarrison = settings.RegicideGarrison;
+ cmpEndGameManager.SetGameSettings(gameSettings);
cmpEndGameManager.SetAlliedVictory(settings.LockTeams || !settings.LastManStanding);
if (settings.LockTeams && settings.LastManStanding)
warn("Last man standing is only available in games with unlocked teams!");
if (settings.Garrison)
for (let holder in settings.Garrison)
{
let cmpGarrisonHolder = Engine.QueryInterface(+holder, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
warn("Map error in Setup.js: entity " + holder + " can not garrison units");
else
cmpGarrisonHolder.initGarrison = settings.Garrison[holder];
}
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (settings.Ceasefire)
cmpCeasefireManager.StartCeasefire(settings.Ceasefire * 60 * 1000);
}
Engine.RegisterGlobal("LoadMapSettings", LoadMapSettings);
Index: ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp
===================================================================
--- ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp (revision 21473)
+++ ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp (revision 21474)
@@ -1,589 +1,652 @@
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "Map.h"
#include "AtlasObject/AtlasObject.h"
#include "GameInterface/Messages.h"
#include "ScenarioEditor/ScenarioEditor.h"
#include "ScenarioEditor/Tools/Common/Tools.h"
#include "wx/busyinfo.h"
#include "wx/filename.h"
#define CREATE_CHECKBOX(window, parentSizer, name, description, ID) \
parentSizer->Add(new wxStaticText(window, wxID_ANY, _(name)), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); \
parentSizer->Add(Tooltipped(new wxCheckBox(window, ID, wxEmptyString), _(description)));
enum
{
ID_MapName,
ID_MapDescription,
ID_MapReveal,
ID_MapType,
ID_MapPreview,
ID_MapTeams,
ID_MapKW_Demo,
ID_MapKW_Naval,
+ ID_VC_Conquest,
+ ID_VC_ConquestUnits,
+ ID_VC_ConquestStructures,
+ ID_VC_CaptureTheRelic,
+ ID_VC_Wonder,
+ ID_VC_Regicide,
ID_RandomScript,
ID_RandomSize,
ID_RandomNomad,
ID_RandomSeed,
ID_RandomReseed,
ID_RandomGenerate,
ID_SimPlay,
ID_SimFast,
ID_SimSlow,
ID_SimPause,
ID_SimReset,
ID_OpenPlayerPanel
};
enum
{
SimInactive,
SimPlaying,
SimPlayingFast,
SimPlayingSlow,
SimPaused
};
bool IsPlaying(int s) { return (s == SimPlaying || s == SimPlayingFast || s == SimPlayingSlow); }
// TODO: Some of these helper things should be moved out of this file
// and into shared locations
// Helper function for adding tooltips
static wxWindow* Tooltipped(wxWindow* window, const wxString& tip)
{
window->SetToolTip(tip);
return window;
}
// Helper class for storing AtObjs
class AtObjClientData : public wxClientData
{
public:
AtObjClientData(const AtObj& obj) : obj(obj) {}
virtual ~AtObjClientData() {}
AtObj GetValue() { return obj; }
private:
AtObj obj;
};
class MapSettingsControl : public wxPanel
{
public:
MapSettingsControl(wxWindow* parent, ScenarioEditor& scenarioEditor);
void CreateWidgets();
void ReadFromEngine();
void SetMapSettings(const AtObj& obj);
AtObj UpdateSettingsObject();
private:
void SendToEngine();
+ void OnConquestChanged();
- void OnEdit(wxCommandEvent& WXUNUSED(evt))
+ void OnEdit(wxCommandEvent& evt)
{
SendToEngine();
+ if (evt.GetId() == ID_VC_Conquest)
+ OnConquestChanged();
}
std::set m_MapSettingsKeywords;
+ std::set m_MapSettingsVictoryConditions;
std::vector m_PlayerCivChoices;
Observable& m_MapSettings;
DECLARE_EVENT_TABLE();
};
BEGIN_EVENT_TABLE(MapSettingsControl, wxPanel)
EVT_TEXT(ID_MapName, MapSettingsControl::OnEdit)
EVT_TEXT(ID_MapDescription, MapSettingsControl::OnEdit)
EVT_TEXT(ID_MapPreview, MapSettingsControl::OnEdit)
EVT_CHECKBOX(wxID_ANY, MapSettingsControl::OnEdit)
EVT_CHOICE(wxID_ANY, MapSettingsControl::OnEdit)
END_EVENT_TABLE();
MapSettingsControl::MapSettingsControl(wxWindow* parent, ScenarioEditor& scenarioEditor)
: wxPanel(parent, wxID_ANY), m_MapSettings(scenarioEditor.GetMapSettings())
{
wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Map settings"));
SetSizer(sizer);
}
void MapSettingsControl::CreateWidgets()
{
wxSizer* sizer = GetSizer();
/////////////////////////////////////////////////////////////////////////
// Map settings
wxBoxSizer* nameSizer = new wxBoxSizer(wxHORIZONTAL);
nameSizer->Add(new wxStaticText(this, wxID_ANY, _("Name")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
nameSizer->Add(8, 0);
nameSizer->Add(Tooltipped(new wxTextCtrl(this, ID_MapName),
_("Displayed name of the map")), wxSizerFlags().Proportion(1));
sizer->Add(nameSizer, wxSizerFlags().Expand());
sizer->Add(0, 2);
sizer->Add(new wxStaticText(this, wxID_ANY, _("Description")));
sizer->Add(Tooltipped(new wxTextCtrl(this, ID_MapDescription, wxEmptyString, wxDefaultPosition, wxSize(-1, 100), wxTE_MULTILINE),
_("Short description used on the map selection screen")), wxSizerFlags().Expand());
sizer->AddSpacer(5);
- // TODO: replace by filenames in binaries/data/mods/public/simulation/data/settings/victory_conditions/
- wxArrayString gameTypes;
- gameTypes.Add(_T("conquest"));
- gameTypes.Add(_T("conquest_structures"));
- gameTypes.Add(_T("conquest_units"));
- gameTypes.Add(_T("wonder"));
- gameTypes.Add(_T("endless"));
- gameTypes.Add(_T("regicide"));
- gameTypes.Add(_T("capture_the_relic"));
-
wxFlexGridSizer* gridSizer = new wxFlexGridSizer(2, 5, 5);
gridSizer->AddGrowableCol(1);
+
// TODO: have preview selector tool?
gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Preview")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
gridSizer->Add(Tooltipped(new wxTextCtrl(this, ID_MapPreview, wxEmptyString),
_("Texture used for map preview")), wxSizerFlags().Expand());
CREATE_CHECKBOX(this, gridSizer, "Reveal map", "If checked, players won't need to explore", ID_MapReveal);
- gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Game type")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
- gridSizer->Add(Tooltipped(new wxChoice(this, ID_MapType, wxDefaultPosition, wxDefaultSize, gameTypes),
- _("Select the game type (or victory condition)")), wxSizerFlags().Expand());
CREATE_CHECKBOX(this, gridSizer, "Lock teams", "If checked, teams will be locked", ID_MapTeams);
sizer->Add(gridSizer, wxSizerFlags().Expand());
sizer->AddSpacer(5);
+ // TODO: replace by names in binaries/data/mods/public/simulation/data/settings/victory_conditions/
+ wxStaticBoxSizer* victoryConditionSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Victory Conditions"));
+ wxFlexGridSizer* vcGridSizer = new wxFlexGridSizer(2, 0, 5);
+ vcGridSizer->AddGrowableCol(1);
+ CREATE_CHECKBOX(this, vcGridSizer, "Conquest", "Select Conquest victory condition", ID_VC_Conquest);
+ CREATE_CHECKBOX(this, vcGridSizer, "Conquest Units", "Select Conquest Units victory condition", ID_VC_ConquestUnits);
+ CREATE_CHECKBOX(this, vcGridSizer, "Conquest Structures", "Select Conquest Structures victory condition", ID_VC_ConquestStructures);
+ CREATE_CHECKBOX(this, vcGridSizer, "Capture the Relic", "Select Capture the Relic victory condition", ID_VC_CaptureTheRelic);
+ CREATE_CHECKBOX(this, vcGridSizer, "Wonder", "Select Wonder victory condition", ID_VC_Wonder);
+ CREATE_CHECKBOX(this, vcGridSizer, "Regicide", "Select Regicide victory condition", ID_VC_Regicide);
+ victoryConditionSizer->Add(vcGridSizer);
+ sizer->Add(victoryConditionSizer, wxSizerFlags().Expand());
+
+ sizer->AddSpacer(5);
+
wxStaticBoxSizer* keywordsSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Keywords"));
wxFlexGridSizer* kwGridSizer = new wxFlexGridSizer(4, 5, 5);
CREATE_CHECKBOX(this, kwGridSizer, "Demo", "If checked, map will only be visible using filters in game setup", ID_MapKW_Demo);
CREATE_CHECKBOX(this, kwGridSizer, "Naval", "If checked, map will only be visible using filters in game setup", ID_MapKW_Naval);
keywordsSizer->Add(kwGridSizer);
sizer->Add(keywordsSizer, wxSizerFlags().Expand());
}
void MapSettingsControl::ReadFromEngine()
{
AtlasMessage::qGetMapSettings qry;
qry.Post();
if (!(*qry.settings).empty())
{
// Prevent error if there's no map settings to parse
m_MapSettings = AtlasObject::LoadFromJSON(*qry.settings);
}
// map name
wxDynamicCast(FindWindow(ID_MapName), wxTextCtrl)->ChangeValue(wxString(m_MapSettings["Name"]));
// map description
wxDynamicCast(FindWindow(ID_MapDescription), wxTextCtrl)->ChangeValue(wxString(m_MapSettings["Description"]));
// map preview
wxDynamicCast(FindWindow(ID_MapPreview), wxTextCtrl)->ChangeValue(wxString(m_MapSettings["Preview"]));
// reveal map
wxDynamicCast(FindWindow(ID_MapReveal), wxCheckBox)->SetValue(wxString(m_MapSettings["RevealMap"]) == L"true");
- // game type / victory conditions
- if (m_MapSettings["GameType"].defined())
- wxDynamicCast(FindWindow(ID_MapType), wxChoice)->SetStringSelection(wxString(m_MapSettings["GameType"]));
- else
- wxDynamicCast(FindWindow(ID_MapType), wxChoice)->SetSelection(0);
+ // victory conditions
+ m_MapSettingsVictoryConditions.clear();
+ for (AtIter victoryCondition = m_MapSettings["VictoryConditions"]["item"]; victoryCondition.defined(); ++victoryCondition)
+ m_MapSettingsVictoryConditions.insert(std::wstring(victoryCondition));
+
+ wxWindow* window;
+#define INIT_CHECKBOX(ID, mapSettings, value) \
+ window = FindWindow(ID); \
+ if (window != nullptr) \
+ wxDynamicCast(window, wxCheckBox)->SetValue(mapSettings.count(value) != 0);
+
+ INIT_CHECKBOX(ID_VC_Conquest, m_MapSettingsVictoryConditions, L"conquest");
+ INIT_CHECKBOX(ID_VC_ConquestUnits, m_MapSettingsVictoryConditions, L"conquest_units");
+ INIT_CHECKBOX(ID_VC_ConquestStructures, m_MapSettingsVictoryConditions, L"conquest_structures");
+ INIT_CHECKBOX(ID_VC_CaptureTheRelic, m_MapSettingsVictoryConditions, L"capture_the_relic");
+ INIT_CHECKBOX(ID_VC_Wonder, m_MapSettingsVictoryConditions, L"wonder");
+ INIT_CHECKBOX(ID_VC_Regicide, m_MapSettingsVictoryConditions, L"regicide");
+ OnConquestChanged();
// lock teams
wxDynamicCast(FindWindow(ID_MapTeams), wxCheckBox)->SetValue(wxString(m_MapSettings["LockTeams"]) == L"true");
// keywords
{
m_MapSettingsKeywords.clear();
for (AtIter keyword = m_MapSettings["Keywords"]["item"]; keyword.defined(); ++keyword)
m_MapSettingsKeywords.insert(std::wstring(keyword));
- wxDynamicCast(FindWindow(ID_MapKW_Demo), wxCheckBox)->SetValue(m_MapSettingsKeywords.count(L"demo") != 0);
- wxDynamicCast(FindWindow(ID_MapKW_Naval), wxCheckBox)->SetValue(m_MapSettingsKeywords.count(L"naval") != 0);
+ INIT_CHECKBOX(ID_MapKW_Demo, m_MapSettingsKeywords, L"demo");
+ INIT_CHECKBOX(ID_MapKW_Naval, m_MapSettingsKeywords, L"naval");
}
+
+#undef INIT_CHECKBOX
}
void MapSettingsControl::SetMapSettings(const AtObj& obj)
{
m_MapSettings = obj;
m_MapSettings.NotifyObservers();
SendToEngine();
}
+// TODO Use the json data for this
+void MapSettingsControl::OnConquestChanged()
+{
+ bool conqestEnabled = wxDynamicCast(FindWindow(ID_VC_Conquest), wxCheckBox)->GetValue();
+
+ wxCheckBox* conquestUnitsCheckbox = wxDynamicCast(FindWindow(ID_VC_ConquestUnits), wxCheckBox);
+ conquestUnitsCheckbox->Enable(!conqestEnabled);
+ wxCheckBox* conquestStructuresCheckbox = wxDynamicCast(FindWindow(ID_VC_ConquestStructures), wxCheckBox);
+ conquestStructuresCheckbox->Enable(!conqestEnabled);
+ if (conqestEnabled)
+ {
+ conquestUnitsCheckbox->SetValue(false);
+ conquestStructuresCheckbox->SetValue(false);
+ }
+}
+
AtObj MapSettingsControl::UpdateSettingsObject()
{
// map name
m_MapSettings.set("Name", wxDynamicCast(FindWindow(ID_MapName), wxTextCtrl)->GetValue());
// map description
m_MapSettings.set("Description", wxDynamicCast(FindWindow(ID_MapDescription), wxTextCtrl)->GetValue());
// map preview
m_MapSettings.set("Preview", wxDynamicCast(FindWindow(ID_MapPreview), wxTextCtrl)->GetValue());
// reveal map
m_MapSettings.setBool("RevealMap", wxDynamicCast(FindWindow(ID_MapReveal), wxCheckBox)->GetValue());
- // game type / victory conditions
- m_MapSettings.set("GameType", wxDynamicCast(FindWindow(ID_MapType), wxChoice)->GetStringSelection());
+ // victory conditions
+#define INSERT_VICTORY_CONDITION_CHECKBOX(name, ID) \
+ if (wxDynamicCast(FindWindow(ID), wxCheckBox)->GetValue()) \
+ m_MapSettingsVictoryConditions.insert(name); \
+ else \
+ m_MapSettingsVictoryConditions.erase(name);
+
+ INSERT_VICTORY_CONDITION_CHECKBOX(L"conquest", ID_VC_Conquest);
+ INSERT_VICTORY_CONDITION_CHECKBOX(L"conquest_units", ID_VC_ConquestUnits);
+ INSERT_VICTORY_CONDITION_CHECKBOX(L"conquest_structures", ID_VC_ConquestStructures);
+ INSERT_VICTORY_CONDITION_CHECKBOX(L"capture_the_relic", ID_VC_CaptureTheRelic);
+ INSERT_VICTORY_CONDITION_CHECKBOX(L"wonder", ID_VC_Wonder);
+ INSERT_VICTORY_CONDITION_CHECKBOX(L"regicide", ID_VC_Regicide);
+
+#undef INSERT_VICTORY_CONDITION_CHECKBOX
+
+ AtObj victoryConditions;
+ victoryConditions.set("@array", L"");
+ for (std::set::iterator it = m_MapSettingsVictoryConditions.begin(); it != m_MapSettingsVictoryConditions.end(); ++it)
+ victoryConditions.add("item", it->c_str());
+ m_MapSettings.set("VictoryConditions", victoryConditions);
// keywords
{
if (wxDynamicCast(FindWindow(ID_MapKW_Demo), wxCheckBox)->GetValue())
m_MapSettingsKeywords.insert(L"demo");
else
m_MapSettingsKeywords.erase(L"demo");
if (wxDynamicCast(FindWindow(ID_MapKW_Naval), wxCheckBox)->GetValue())
m_MapSettingsKeywords.insert(L"naval");
else
m_MapSettingsKeywords.erase(L"naval");
AtObj keywords;
keywords.set("@array", L"");
for (std::set::iterator it = m_MapSettingsKeywords.begin(); it != m_MapSettingsKeywords.end(); ++it)
keywords.add("item", it->c_str());
m_MapSettings.set("Keywords", keywords);
}
// teams locked
m_MapSettings.setBool("LockTeams", wxDynamicCast(FindWindow(ID_MapTeams), wxCheckBox)->GetValue());
// default AI RNG seed
m_MapSettings.setInt("AISeed", 0);
return m_MapSettings;
}
void MapSettingsControl::SendToEngine()
{
UpdateSettingsObject();
std::string json = AtlasObject::SaveToJSON(m_MapSettings);
// TODO: would be nice if we supported undo for settings changes
POST_COMMAND(SetMapSettings, (json));
}
MapSidebar::MapSidebar(ScenarioEditor& scenarioEditor, wxWindow* sidebarContainer, wxWindow* bottomBarContainer)
: Sidebar(scenarioEditor, sidebarContainer, bottomBarContainer), m_SimState(SimInactive)
{
wxSizer* scrollSizer = new wxBoxSizer(wxVERTICAL);
wxScrolledWindow* scrolledWindow = new wxScrolledWindow(this);
scrolledWindow->SetScrollRate(10, 10);
scrolledWindow->SetSizer(scrollSizer);
m_MainSizer->Add(scrolledWindow, wxSizerFlags().Expand().Proportion(1));
m_MapSettingsCtrl = new MapSettingsControl(scrolledWindow, m_ScenarioEditor);
scrollSizer->Add(m_MapSettingsCtrl, wxSizerFlags().Expand());
{
/////////////////////////////////////////////////////////////////////////
// Random map settings
wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, scrolledWindow, _("Random map"));
scrollSizer->Add(sizer, wxSizerFlags().Expand());
sizer->Add(new wxChoice(scrolledWindow, ID_RandomScript), wxSizerFlags().Expand());
sizer->AddSpacer(5);
sizer->Add(new wxButton(scrolledWindow, ID_OpenPlayerPanel, _T("Change players")), wxSizerFlags().Expand());
sizer->AddSpacer(5);
wxFlexGridSizer* gridSizer = new wxFlexGridSizer(2, 5, 5);
gridSizer->AddGrowableCol(1);
wxChoice* sizeChoice = new wxChoice(scrolledWindow, ID_RandomSize);
gridSizer->Add(new wxStaticText(scrolledWindow, wxID_ANY, _("Map size")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
gridSizer->Add(sizeChoice, wxSizerFlags().Expand());
CREATE_CHECKBOX(scrolledWindow, gridSizer, "Nomad", "Place only some units instead of starting bases.", ID_RandomNomad);
gridSizer->Add(new wxStaticText(scrolledWindow, wxID_ANY, _("Random seed")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
wxBoxSizer* seedSizer = new wxBoxSizer(wxHORIZONTAL);
seedSizer->Add(Tooltipped(new wxTextCtrl(scrolledWindow, ID_RandomSeed, _T("0"), wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_NUMERIC)),
_("Seed value for random map")), wxSizerFlags(1).Expand());
seedSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_RandomReseed, _("R"), wxDefaultPosition, wxSize(40, -1)),
_("New random seed")));
gridSizer->Add(seedSizer, wxSizerFlags().Expand());
sizer->Add(gridSizer, wxSizerFlags().Expand());
sizer->AddSpacer(5);
sizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_RandomGenerate, _("Generate map")),
_("Run selected random map script")), wxSizerFlags().Expand());
}
{
/////////////////////////////////////////////////////////////////////////
// Simulation buttons
wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, scrolledWindow, _("Simulation test"));
scrollSizer->Add(sizer, wxSizerFlags().Expand().Border(wxTOP, 8));
wxGridSizer* gridSizer = new wxGridSizer(5);
gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimPlay, _("Play"), wxDefaultPosition, wxSize(48, -1)),
_("Run the simulation at normal speed")), wxSizerFlags().Expand());
gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimFast, _("Fast"), wxDefaultPosition, wxSize(48, -1)),
_("Run the simulation at 8x speed")), wxSizerFlags().Expand());
gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimSlow, _("Slow"), wxDefaultPosition, wxSize(48, -1)),
_("Run the simulation at 1/8x speed")), wxSizerFlags().Expand());
gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimPause, _("Pause"), wxDefaultPosition, wxSize(48, -1)),
_("Pause the simulation")), wxSizerFlags().Expand());
gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimReset, _("Reset"), wxDefaultPosition, wxSize(48, -1)),
_("Reset the editor to initial state")), wxSizerFlags().Expand());
sizer->Add(gridSizer, wxSizerFlags().Expand());
UpdateSimButtons();
}
}
void MapSidebar::OnCollapse(wxCollapsiblePaneEvent& WXUNUSED(evt))
{
Freeze();
// Toggling the collapsing doesn't seem to update the sidebar layout
// automatically, so do it explicitly here
Layout();
Refresh(); // fixes repaint glitch on Windows
Thaw();
}
void MapSidebar::OnFirstDisplay()
{
// We do this here becase messages are used which requires simulation to be init'd
m_MapSettingsCtrl->CreateWidgets();
m_MapSettingsCtrl->ReadFromEngine();
// Load the map sizes list
AtlasMessage::qGetMapSizes qrySizes;
qrySizes.Post();
AtObj sizes = AtlasObject::LoadFromJSON(*qrySizes.sizes);
wxChoice* sizeChoice = wxDynamicCast(FindWindow(ID_RandomSize), wxChoice);
for (AtIter s = sizes["Data"]["item"]; s.defined(); ++s)
{
long tiles = 0;
wxString(s["Tiles"]).ToLong(&tiles);
sizeChoice->Append(wxString(s["Name"]), (void*)(intptr_t)tiles);
}
sizeChoice->SetSelection(0);
// Load the RMS script list
AtlasMessage::qGetRMSData qry;
qry.Post();
std::vector scripts = *qry.data;
wxChoice* scriptChoice = wxDynamicCast(FindWindow(ID_RandomScript), wxChoice);
scriptChoice->Clear();
for (size_t i = 0; i < scripts.size(); ++i)
{
AtObj data = AtlasObject::LoadFromJSON(scripts[i]);
wxString name(data["settings"]["Name"]);
if (!name.IsEmpty())
scriptChoice->Append(name, new AtObjClientData(*data["settings"]));
}
scriptChoice->SetSelection(0);
Layout();
}
void MapSidebar::OnMapReload()
{
m_MapSettingsCtrl->ReadFromEngine();
// Reset sim test buttons
POST_MESSAGE(SimPlay, (0.f, false));
POST_MESSAGE(SimStopMusic, ());
POST_MESSAGE(GuiSwitchPage, (L"page_atlas.xml"));
m_SimState = SimInactive;
UpdateSimButtons();
}
void MapSidebar::UpdateSimButtons()
{
wxButton* button;
button = wxDynamicCast(FindWindow(ID_SimPlay), wxButton);
wxCHECK(button, );
button->Enable(m_SimState != SimPlaying);
button = wxDynamicCast(FindWindow(ID_SimFast), wxButton);
wxCHECK(button, );
button->Enable(m_SimState != SimPlayingFast);
button = wxDynamicCast(FindWindow(ID_SimSlow), wxButton);
wxCHECK(button, );
button->Enable(m_SimState != SimPlayingSlow);
button = wxDynamicCast(FindWindow(ID_SimPause), wxButton);
wxCHECK(button, );
button->Enable(IsPlaying(m_SimState));
button = wxDynamicCast(FindWindow(ID_SimReset), wxButton);
wxCHECK(button, );
button->Enable(m_SimState != SimInactive);
}
void MapSidebar::OnSimPlay(wxCommandEvent& event)
{
float speed = 1.f;
int newState = SimPlaying;
if (event.GetId() == ID_SimFast)
{
speed = 8.f;
newState = SimPlayingFast;
}
else if (event.GetId() == ID_SimSlow)
{
speed = 0.125f;
newState = SimPlayingSlow;
}
if (m_SimState == SimInactive)
{
// Force update of player settings
POST_MESSAGE(LoadPlayerSettings, (false));
POST_MESSAGE(SimStateSave, (L"default"));
POST_MESSAGE(GuiSwitchPage, (L"page_session.xml"));
POST_MESSAGE(SimPlay, (speed, true));
m_SimState = newState;
}
else // paused or already playing at a different speed
{
POST_MESSAGE(SimPlay, (speed, true));
m_SimState = newState;
}
UpdateSimButtons();
}
void MapSidebar::OnSimPause(wxCommandEvent& WXUNUSED(event))
{
if (IsPlaying(m_SimState))
{
POST_MESSAGE(SimPlay, (0.f, true));
m_SimState = SimPaused;
}
UpdateSimButtons();
}
void MapSidebar::OnSimReset(wxCommandEvent& WXUNUSED(event))
{
if (IsPlaying(m_SimState))
{
POST_MESSAGE(SimPlay, (0.f, true));
POST_MESSAGE(SimStateRestore, (L"default"));
POST_MESSAGE(SimStopMusic, ());
POST_MESSAGE(SimPlay, (0.f, false));
POST_MESSAGE(GuiSwitchPage, (L"page_atlas.xml"));
m_SimState = SimInactive;
}
else if (m_SimState == SimPaused)
{
POST_MESSAGE(SimPlay, (0.f, true));
POST_MESSAGE(SimStateRestore, (L"default"));
POST_MESSAGE(SimStopMusic, ());
POST_MESSAGE(SimPlay, (0.f, false));
POST_MESSAGE(GuiSwitchPage, (L"page_atlas.xml"));
m_SimState = SimInactive;
}
UpdateSimButtons();
}
void MapSidebar::OnRandomReseed(wxCommandEvent& WXUNUSED(evt))
{
// Pick a shortish randomish value
wxString seed;
seed << (int)floor((rand() / (float)RAND_MAX) * 10000.f);
wxDynamicCast(FindWindow(ID_RandomSeed), wxTextCtrl)->SetValue(seed);
}
void MapSidebar::OnRandomGenerate(wxCommandEvent& WXUNUSED(evt))
{
if (m_ScenarioEditor.DiscardChangesDialog())
return;
wxChoice* scriptChoice = wxDynamicCast(FindWindow(ID_RandomScript), wxChoice);
if (scriptChoice->GetSelection() < 0)
return;
// TODO: this settings thing seems a bit of a mess,
// since it's mixing data from three different sources
AtObj settings = m_MapSettingsCtrl->UpdateSettingsObject();
AtObj scriptSettings = dynamic_cast(scriptChoice->GetClientObject(scriptChoice->GetSelection()))->GetValue();
settings.addOverlay(scriptSettings);
wxChoice* sizeChoice = wxDynamicCast(FindWindow(ID_RandomSize), wxChoice);
wxString size;
size << (intptr_t)sizeChoice->GetClientData(sizeChoice->GetSelection());
settings.setInt("Size", wxAtoi(size));
settings.setBool("Nomad", wxDynamicCast(FindWindow(ID_RandomNomad), wxCheckBox)->GetValue());
settings.setInt("Seed", wxAtoi(wxDynamicCast(FindWindow(ID_RandomSeed), wxTextCtrl)->GetValue()));
std::string json = AtlasObject::SaveToJSON(settings);
wxBusyInfo busy(_("Generating map"));
wxBusyCursor busyc;
wxString scriptName(settings["Script"]);
// Copy the old map settings, so we don't lose them if the map generation fails
AtObj oldSettings = settings;
AtlasMessage::qGenerateMap qry((std::wstring)scriptName.wc_str(), json);
qry.Post();
if (qry.status < 0)
{
// Display error message and revert to old map settings
wxLogError(_("Random map script '%ls' failed"), scriptName.wc_str());
m_MapSettingsCtrl->SetMapSettings(oldSettings);
}
m_ScenarioEditor.NotifyOnMapReload();
}
void MapSidebar::OnOpenPlayerPanel(wxCommandEvent& WXUNUSED(evt))
{
m_ScenarioEditor.SelectPage(_T("PlayerSidebar"));
}
BEGIN_EVENT_TABLE(MapSidebar, Sidebar)
EVT_COLLAPSIBLEPANE_CHANGED(wxID_ANY, MapSidebar::OnCollapse)
EVT_BUTTON(ID_SimPlay, MapSidebar::OnSimPlay)
EVT_BUTTON(ID_SimFast, MapSidebar::OnSimPlay)
EVT_BUTTON(ID_SimSlow, MapSidebar::OnSimPlay)
EVT_BUTTON(ID_SimPause, MapSidebar::OnSimPause)
EVT_BUTTON(ID_SimReset, MapSidebar::OnSimReset)
EVT_BUTTON(ID_RandomReseed, MapSidebar::OnRandomReseed)
EVT_BUTTON(ID_RandomGenerate, MapSidebar::OnRandomGenerate)
EVT_BUTTON(ID_OpenPlayerPanel, MapSidebar::OnOpenPlayerPanel)
END_EVENT_TABLE();
Index: ps/trunk/binaries/data/mods/public/gui/common/settings.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 21474)
@@ -1,438 +1,429 @@
/**
* The maximum number of players that the engine supports.
* TODO: Maybe we can support more than 8 players sometime.
*/
const g_MaxPlayers = 8;
/**
* The maximum number of teams allowed.
*/
const g_MaxTeams = 4;
/**
* Directory containing all editable settings.
*/
const g_SettingsDirectory = "simulation/data/settings/";
/**
* Directory containing all biomes supported for random map scripts.
*/
const g_BiomesDirectory = "maps/random/rmbiome/";
/**
* An object containing all values given by setting name.
* Used by lobby, gamesetup, session, summary screen and replay menu.
*/
const g_Settings = loadSettingsValues();
/**
* Loads and translates all values of all settings which
* can be configured by dropdowns in the gamesetup.
*
* @returns {Object|undefined}
*/
function loadSettingsValues()
{
var settings = {
"AIDescriptions": loadAIDescriptions(),
"AIDifficulties": loadAIDifficulties(),
"AIBehaviors": loadAIBehaviors(),
"Ceasefire": loadCeasefire(),
"VictoryDurations": loadVictoryDuration(),
"GameSpeeds": loadSettingValuesFile("game_speeds.json"),
"MapTypes": loadMapTypes(),
"MapSizes": loadSettingValuesFile("map_sizes.json"),
"Biomes": loadBiomes(),
"PlayerDefaults": loadPlayerDefaults(),
"PopulationCapacities": loadPopulationCapacities(),
"StartingResources": loadSettingValuesFile("starting_resources.json"),
"VictoryConditions": loadVictoryConditions(),
"TriggerDifficulties": loadSettingValuesFile("trigger_difficulties.json")
};
if (Object.keys(settings).some(key => settings[key] === undefined))
return undefined;
return deepfreeze(settings);
}
/**
* Returns an array of objects reflecting all possible values for a given setting.
*
* @param {string} filename
* @see simulation/data/settings/
* @returns {Array|undefined}
*/
function loadSettingValuesFile(filename)
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + filename);
if (!json || !json.Data)
{
error("Could not load " + filename + "!");
return undefined;
}
if (json.TranslatedKeys)
{
let keyContext = json.TranslatedKeys;
if (json.TranslationContext)
{
keyContext = {};
for (let key of json.TranslatedKeys)
keyContext[key] = json.TranslationContext;
}
translateObjectKeys(json.Data, keyContext);
}
return json.Data;
}
/**
* Loads the descriptions as defined in simulation/ai/.../data.json and loaded by ICmpAIManager.cpp.
*
* @returns {Array}
*/
function loadAIDescriptions()
{
var ais = Engine.GetAIs();
translateObjectKeys(ais, ["name", "description"]);
return ais.sort((a, b) => a.data.name.localeCompare(b.data.name));
}
/**
* Hardcoded, as modding is not supported without major changes.
* Notice the AI code parses the difficulty level by the index, not by name.
*
* @returns {Array}
*/
function loadAIDifficulties()
{
return [
{
"Name": "sandbox",
"Title": translateWithContext("aiDiff", "Sandbox")
},
{
"Name": "very easy",
"Title": translateWithContext("aiDiff", "Very Easy")
},
{
"Name": "easy",
"Title": translateWithContext("aiDiff", "Easy")
},
{
"Name": "medium",
"Title": translateWithContext("aiDiff", "Medium"),
"Default": true
},
{
"Name": "hard",
"Title": translateWithContext("aiDiff", "Hard")
},
{
"Name": "very hard",
"Title": translateWithContext("aiDiff", "Very Hard")
}
];
}
function loadAIBehaviors()
{
return [
{
"Name": "random",
"Title": translateWithContext("aiBehavior", "Random"),
"Default": true
},
{
"Name": "balanced",
"Title": translateWithContext("aiBehavior", "Balanced"),
},
{
"Name": "defensive",
"Title": translateWithContext("aiBehavior", "Defensive")
},
{
"Name": "aggressive",
"Title": translateWithContext("aiBehavior", "Aggressive")
}
];
}
/**
* Loads available victory times for victory conditions like Wonder and Capture the Relic.
*/
function loadVictoryDuration()
{
var jsonFile = "victory_times.json";
var json = Engine.ReadJSONFile(g_SettingsDirectory + jsonFile);
if (!json || json.Default === undefined || !json.Times || !Array.isArray(json.Times))
{
error("Could not load " + jsonFile);
return undefined;
}
return json.Times.map(duration => ({
"Duration": duration,
"Default": duration == json.Default,
"Title": sprintf(translatePluralWithContext("victory duration", "%(min)s minute", "%(min)s minutes", duration), { "min": duration })
}));
}
/**
* Loads available ceasefire settings.
*
* @returns {Array|undefined}
*/
function loadCeasefire()
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + "ceasefire.json");
if (!json || json.Default === undefined || !json.Times || !Array.isArray(json.Times))
{
error("Could not load ceasefire.json");
return undefined;
}
return json.Times.map(timeout => ({
"Duration": timeout,
"Default": timeout == json.Default,
"Title": timeout == 0 ? translateWithContext("ceasefire", "No ceasefire") :
sprintf(translatePluralWithContext("ceasefire", "%(minutes)s minute", "%(minutes)s minutes", timeout), { "minutes": timeout })
}));
}
/**
* Hardcoded, as modding is not supported without major changes.
*
* @returns {Array}
*/
function loadMapTypes()
{
return [
{
"Name": "skirmish",
"Title": translateWithContext("map", "Skirmish"),
"Description": translate("A map with a predefined landscape and number of players. Freely select the other gamesettings."),
"Default": true
},
{
"Name": "random",
"Title": translateWithContext("map", "Random"),
"Description": translate("Create a unique map with a different resource distribution each time. Freely select the number of players and teams.")
},
{
"Name": "scenario",
"Title": translateWithContext("map", "Scenario"),
"Description": translate("A map with a predefined landscape and matchsettings.")
}
];
}
function loadBiomes()
{
return listFiles(g_BiomesDirectory, ".json", true).filter(biomeID => biomeID != "defaultbiome").map(biomeID => {
let description = Engine.ReadJSONFile(g_BiomesDirectory + biomeID + ".json").Description;
return {
"Id": biomeID,
"Title": translateWithContext("biome definition", description.Title),
"Description": description.Description ? translateWithContext("biome definition", description.Description) : ""
};
});
}
/**
- * Loads available gametypes.
+ * Loads available victoryCondtions from json files.
*
* @returns {Array|undefined}
*/
function loadVictoryConditions()
{
let subdir = "victory_conditions/";
let victoryConditions = listFiles(g_SettingsDirectory + subdir, ".json", false).map(victoryScriptName => {
- let vc = loadSettingValuesFile(subdir + victoryScriptName + ".json");
- if (vc)
- vc.Name = victoryScriptName;
- return vc;
+ let victoryCondition = loadSettingValuesFile(subdir + victoryScriptName + ".json");
+ if (victoryCondition)
+ victoryCondition.Name = victoryScriptName;
+ return victoryCondition;
});
- if (victoryConditions.some(vc => vc == undefined))
+ if (victoryConditions.some(victoryCondition => victoryCondition == undefined))
return undefined;
- // TODO: We might support enabling victory conditions separately sometime.
- // Until then, we supplement the endless gametype here.
- victoryConditions.push({
- "Name": "endless",
- "Title": translateWithContext("victory condition", "None"),
- "Description": translate("Endless game."),
- "Scripts": []
- });
-
return victoryConditions;
}
/**
* Loads the default player settings (like civs and colors).
*
* @returns {Array|undefined}
*/
function loadPlayerDefaults()
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + "player_defaults.json");
if (!json || !json.PlayerData)
{
error("Could not load player_defaults.json");
return undefined;
}
return json.PlayerData;
}
/**
* Loads available population capacities.
*
* @returns {Array|undefined}
*/
function loadPopulationCapacities()
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + "population_capacities.json");
if (!json || json.Default === undefined || !json.PopulationCapacities || !Array.isArray(json.PopulationCapacities))
{
error("Could not load population_capacities.json");
return undefined;
}
return json.PopulationCapacities.map(population => ({
"Population": population,
"Default": population == json.Default,
"Title": population < 10000 ? population : translate("Unlimited")
}));
}
/**
* Creates an object with all values of that property of the given setting and
* finds the index of the default value.
*
* This allows easy copying of setting values to dropdown lists.
*
* @param {Array} settingValues
* @returns {Object|undefined}
*/
function prepareForDropdown(settingValues)
{
if (!settingValues)
return undefined;
let settings = { "Default": 0 };
for (let index in settingValues)
{
for (let property in settingValues[index])
{
if (property == "Default")
continue;
if (!settings[property])
settings[property] = [];
// Switch property and index
settings[property][index] = settingValues[index][property];
}
// Copy default value
if (settingValues[index].Default)
settings.Default = +index;
}
return deepfreeze(settings);
}
function getGameSpeedChoices(allowFastForward)
{
return prepareForDropdown(g_Settings.GameSpeeds.filter(speed => !speed.FastForward || allowFastForward));
}
/**
* Returns title or placeholder.
*
* @param {string} aiName - for example "petra"
*/
function translateAIName(aiName)
{
let description = g_Settings.AIDescriptions.find(ai => ai.id == aiName);
return description ? translate(description.data.name) : translateWithContext("AI name", "Unknown");
}
/**
* Returns title or placeholder.
*
* @param {Number} index - index of AIDifficulties
*/
function translateAIDifficulty(index)
{
let difficulty = g_Settings.AIDifficulties[index];
return difficulty ? difficulty.Title : translateWithContext("AI difficulty", "Unknown");
}
/**
* Returns title or placeholder.
*
* @param {string} aiBehavior - for example "defensive"
*/
function translateAIBehavior(aiBehavior)
{
let behavior = g_Settings.AIBehaviors.find(b => b.Name == aiBehavior);
return behavior ? behavior.Title : translateWithContext("AI behavior", "Default");
}
/**
* Returns title or placeholder.
*
* @param {string} mapType - for example "skirmish"
* @returns {string}
*/
function translateMapType(mapType)
{
let type = g_Settings.MapTypes.find(t => t.Name == mapType);
return type ? type.Title : translateWithContext("map type", "Unknown");
}
/**
* Returns title or placeholder "Default".
*
* @param {Number} mapSize - tilecount
* @returns {string}
*/
function translateMapSize(tiles)
{
let mapSize = g_Settings.MapSizes.find(size => size.Tiles == +tiles);
return mapSize ? mapSize.Name : translateWithContext("map size", "Default");
}
/**
* Returns title or placeholder.
*
* @param {Number} population - for example 300
* @returns {string}
*/
function translatePopulationCapacity(population)
{
let popCap = g_Settings.PopulationCapacities.find(p => p.Population == population);
return popCap ? popCap.Title : translateWithContext("population capacity", "Unknown");
}
/**
* Returns title or placeholder.
*
- * @param {string} gameType - for example "conquest"
+ * @param {string} victoryConditionName - For example "conquest".
* @returns {string}
*/
-function translateVictoryCondition(gameType)
+function translateVictoryCondition(victoryConditionName)
{
- let victoryCondition = g_Settings.VictoryConditions.find(vc => vc.Name == gameType);
- return victoryCondition ? victoryCondition.Title : translateWithContext("victory condition", "Unknown");
+ let victoryCondition = g_Settings.VictoryConditions.find(victoryCondition => victoryCondition.Name == victoryConditionName);
+ return victoryCondition ? victoryCondition.Title : translate("Unknown Victory Condition");
}
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 21474)
@@ -1,2703 +1,2712 @@
const g_MatchSettings_SP = "config/matchsettings.json";
const g_MatchSettings_MP = "config/matchsettings.mp.json";
const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_TriggerDifficulties = prepareForDropdown(g_Settings && g_Settings.TriggerDifficulties);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
-const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions);
const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
+const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
+
var g_GameSpeeds = getGameSpeedChoices(false);
/**
* Offer users to select playable civs only.
* Load unselectable civs as they could appear in scenario maps.
*/
const g_CivData = loadCivData(false, false);
/**
* Store civilization code and page (structree or history) opened in civilization info.
*/
var g_CivInfo = {
"code": "",
"page": "page_civinfo.xml"
};
/**
* Highlight the "random" dropdownlist item.
*/
var g_ColorRandom = "orange";
/**
* Color for regular dropdownlist items.
*/
var g_ColorRegular = "white";
/**
* Color for "Unassigned"-placeholder item in the dropdownlist.
*/
var g_PlayerAssignmentColors = {
"player": g_ColorRegular,
"observer": "170 170 250",
"unassigned": "140 140 140",
"AI": "70 150 70"
};
/**
* Used for highlighting the sender of chat messages.
*/
var g_SenderFont = "sans-bold-13";
/**
* This yields [1, 2, ..., MaxPlayers].
*/
var g_NumPlayersList = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1);
/**
* Used for generating the botnames.
*/
var g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
var g_PlayerTeamList = prepareForDropdown([{
"label": translateWithContext("team", "None"),
"id": -1
}].concat(
Array(g_MaxTeams).fill(0).map((v, i) => ({
"label": i + 1,
"id": i
}))
)
);
/**
* Number of relics: [1, ..., NumCivs]
*/
var g_RelicCountList = Object.keys(g_CivData).map((civ, i) => i + 1);
var g_PlayerCivList = g_CivData && prepareForDropdown([{
"name": translateWithContext("civilization", "Random"),
"tooltip": translate("Picks one civilization at random when the game starts."),
"color": g_ColorRandom,
"code": "random"
}].concat(
Object.keys(g_CivData).filter(
civ => g_CivData[civ].SelectableInGameSetup
).map(civ => ({
"name": g_CivData[civ].Name,
"tooltip": g_CivData[civ].History,
"color": g_ColorRegular,
"code": civ
})).sort(sortNameIgnoreCase)
)
);
/**
* All selectable playercolors except gaia.
*/
var g_PlayerColorPickerList = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
/**
* Directory containing all maps of the given type.
*/
var g_MapPath = {
"random": "maps/random/",
"scenario": "maps/scenarios/",
"skirmish": "maps/skirmishes/"
};
/**
* Containing the colors to highlight the ready status of players,
* the chat ready messages and
* the tooltips and captions for the ready button
*/
var g_ReadyData = [
{
"color": g_ColorRegular,
"chat": translate("* %(username)s is not ready."),
"caption": translate("I'm ready"),
"tooltip": translate("State that you are ready to play.")
},
{
"color": "green",
"chat": translate("* %(username)s is ready!"),
"caption": translate("Stay ready"),
"tooltip": translate("Stay ready even when the game settings change.")
},
{
"color": "150 150 250",
"chat": "",
"caption": translate("I'm not ready!"),
"tooltip": translate("State that you are not ready to play.")
}
];
/**
* Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
*/
var g_NetMessageTypes = {
"netstatus": msg => handleNetStatusMessage(msg),
"netwarn": msg => addNetworkWarning(msg),
"gamesetup": msg => handleGamesetupMessage(msg),
"players": msg => handlePlayerAssignmentMessage(msg),
"ready": msg => handleReadyMessage(msg),
"start": msg => handleGamestartMessage(msg),
"kicked": msg => addChatMessage({
"type": msg.banned ? "banned" : "kicked",
"username": msg.username
}),
"chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text }),
};
var g_FormatChatMessage = {
"system": (msg, user) => systemMessage(msg.text),
"settings": (msg, user) => systemMessage(translate('Game settings have been changed')),
"connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })),
"disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })),
"kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })),
"banned": (msg, user) => systemMessage(sprintf(translate("%(username)s has been banned"), { "username": user })),
"chat": (msg, user) => sprintf(translate("%(username)s %(message)s"), {
"username": senderFont(sprintf(translate("<%(username)s>"), { "username": user })),
"message": escapeText(msg.text || "")
}),
"ready": (msg, user) => sprintf(g_ReadyData[msg.status].chat, { "username": user }),
"clientlist": (msg, user) => getUsernameList(),
};
var g_MapFilters = [
{
"id": "default",
"name": translateWithContext("map filter", "Default"),
"tooltip": translateWithContext("map filter", "All maps except naval and demo maps."),
"filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1),
"Default": true
},
{
"id": "naval",
"name": translate("Naval Maps"),
"tooltip": translateWithContext("map filter", "Maps where ships are needed to reach the enemy."),
"filter": mapKeywords => mapKeywords.indexOf("naval") != -1
},
{
"id": "demo",
"name": translate("Demo Maps"),
"tooltip": translateWithContext("map filter", "These maps are not playable but for demonstration purposes only."),
"filter": mapKeywords => mapKeywords.indexOf("demo") != -1
},
{
"id": "new",
"name": translate("New Maps"),
"tooltip": translateWithContext("map filter", "Maps that are brand new in this release of the game."),
"filter": mapKeywords => mapKeywords.indexOf("new") != -1
},
{
"id": "trigger",
"name": translate("Trigger Maps"),
"tooltip": translateWithContext("map filter", "Maps that come with scripted events and potentially spawn enemy units."),
"filter": mapKeywords => mapKeywords.indexOf("trigger") != -1
},
{
"id": "all",
"name": translate("All Maps"),
"tooltip": translateWithContext("map filter", "Every map of the chosen maptype."),
"filter": mapKeywords => true
},
];
/**
* This contains only filters that have at least one map matching them.
*/
var g_MapFilterList;
/**
* Array of biome identifiers supported by the currently selected map.
*/
var g_BiomeList;
/**
* Array of trigger difficulties identifiers supported by the currently selected map.
*/
var g_TriggerDifficultyList;
/**
* Whether this is a single- or multiplayer match.
*/
var g_IsNetworked;
/**
* Is this user in control of game settings (i.e. singleplayer or host of a multiplayergame).
*/
var g_IsController;
/**
* Whether this is a tutorial.
*/
var g_IsTutorial;
/**
* To report the game to the lobby bot.
*/
var g_ServerName;
var g_ServerPort;
/**
* IP address and port of the STUN endpoint.
*/
var g_StunEndpoint;
/**
* States whether the GUI is currently updated in response to network messages instead of user input
* and therefore shouldn't send further messages to the network.
*/
var g_IsInGuiUpdate = false;
/**
* Whether the current player is ready to start the game.
* 0 - not ready
* 1 - ready
* 2 - stay ready
*/
var g_IsReady = 0;
/**
* Ignore duplicate ready commands on init.
*/
var g_ReadyInit = true;
/**
* If noone has changed the ready status, we have no need to spam the settings changed message.
*
* <=0 - Suppressed settings message
* 1 - Will show settings message
* 2 - Host's initial ready, suppressed settings message
*/
var g_ReadyChanged = 2;
/**
* Used to prevent calling resetReadyData when starting a game.
*/
var g_GameStarted = false;
/**
* Selectable options (player, AI, unassigned) in the player assignment dropdowns and
* their colorized, textual representation.
*/
var g_PlayerAssignmentList = {};
/**
* Remembers which clients are assigned to which player slots and whether they are ready.
* The keys are guids or "local" in Singleplayer.
*/
var g_PlayerAssignments = {};
var g_DefaultPlayerData = [];
var g_GameAttributes = { "settings": {} };
/**
* List of translated words that can be used to autocomplete titles of settings
* and their values (for example playernames).
*/
var g_Autocomplete = [];
/**
* Array of strings formatted as displayed, including playername.
*/
var g_ChatMessages = [];
/**
* Minimum amount of pixels required for the chat panel to be visible.
*/
var g_MinChatWidth = 96;
/**
* Horizontal space between chat window and settings.
*/
var g_ChatSettingsMargin = 10;
/**
* Filename and translated title of all maps, given the currently selected
* maptype and filter. Sorted by title, shown in the dropdown.
*/
var g_MapSelectionList = [];
/**
* Cache containing the mapsettings. Just-in-time loading.
*/
var g_MapData = {};
/**
* Wait one tick before initializing the GUI objects and
* don't process netmessages prior to that.
*/
var g_LoadingState = 0;
/**
* Send the current gamesettings to the lobby bot if the settings didn't change for this number of seconds.
*/
var g_GameStanzaTimeout = 2;
/**
* Index of the GUI timer.
*/
var g_GameStanzaTimer;
/**
* Only send a lobby update if something actually changed.
*/
var g_LastGameStanza;
/**
* Remembers if the current player viewed the AI settings of some playerslot.
*/
var g_LastViewedAIPlayer = -1;
/**
* Total number of units that the engine can run with smoothly.
* It means a 4v4 with 150 population can still run nicely, but more than that might "lag".
*/
var g_PopulationCapacityRecommendation = 1200;
/**
* Horizontal space between tab buttons and lobby button.
*/
var g_LobbyButtonSpacing = 8;
/**
* Vertical size of a tab button.
*/
var g_TabButtonHeight = 30;
/**
* Vertical space between two tab buttons.
*/
var g_TabButtonDist = 4;
/**
* Vertical size of a setting object.
*/
var g_SettingHeight = 32;
/**
* Vertical space between two setting objects.
*/
var g_SettingDist = 2;
/**
* Maximum width of a column in the settings panel.
*/
var g_MaxColumnWidth = 470;
/**
* Pixels per millisecond the settings panel slides when opening/closing.
*/
var g_SlideSpeed = 1.2;
/**
* Store last tick time.
*/
var g_LastTickTime = Date.now();
/**
* Order in which the GUI elements will be shown.
* All valid settings are required to appear here.
*/
var g_SettingsTabsGUI = [
{
"label": translateWithContext("Match settings tab name", "Map"),
"settings": [
"mapType",
"mapFilter",
"mapSelection",
"mapSize",
"biome",
"nomad",
"triggerDifficulty",
"disableTreasures",
"exploreMap",
"revealMap"
]
},
{
"label": translateWithContext("Match settings tab name", "Player"),
"settings": [
"numPlayers",
"populationCap",
"startingResources",
"disableSpies",
"enableCheats"
]
},
{
"label": translateWithContext("Match settings tab name", "Game Type"),
"settings": [
- "victoryCondition",
+ ...g_VictoryConditions.map(victoryCondition => victoryCondition.Name),
"relicCount",
"relicDuration",
"wonderDuration",
"regicideGarrison",
"gameSpeed",
"ceasefire",
"lockTeams",
"lastManStanding",
"enableRating"
]
}
];
/**
* Contains the logic of all multiple-choice gamesettings.
*
* Logic
* ids - Array of identifier strings that indicate the selected value.
* default - Returns the index of the default value (not the value itself).
* defined - Whether a value for the setting is actually specified.
* get - The identifier of the currently selected value.
* select - Saves and processes the value of the selected index of the ids array.
*
* GUI
* title - The caption shown in the label.
* tooltip - A description shown when hovering the dropdown or a specific item.
* labels - Array of translated strings selectable for this dropdown.
* colors - Optional array of colors to tint the according dropdown items with.
* hidden - If hidden, both the label and dropdown won't be visible. (default: false)
* enabled - Only the label will be shown if the setting is disabled. (default: true)
* autocomplete - Marks whether to autocomplete translated values of the string. (default: undefined)
* If not undefined, must be a number that denotes the priority (higher numbers come first).
* If undefined, still autocompletes the translated title of the setting.
* initOrder - Settings with lower values will be initialized first.
*/
var g_Dropdowns = {
"mapType": {
"title": () => translate("Map Type"),
"tooltip": (hoverIdx) => g_MapTypes.Description[hoverIdx] || translate("Select a map type."),
"labels": () => g_MapTypes.Title,
"ids": () => g_MapTypes.Name,
"default": () => g_MapTypes.Default,
"defined": () => g_GameAttributes.mapType !== undefined,
"get": () => g_GameAttributes.mapType,
"select": (itemIdx) => {
g_MapData = {};
g_GameAttributes.mapType = g_MapTypes.Name[itemIdx];
g_GameAttributes.mapPath = g_MapPath[g_GameAttributes.mapType];
delete g_GameAttributes.map;
if (g_GameAttributes.mapType != "scenario")
g_GameAttributes.settings = {
"PlayerData": clone(g_DefaultPlayerData.slice(0, 4))
};
reloadMapFilterList();
},
"autocomplete": 0,
"initOrder": 1
},
"mapFilter": {
"title": () => translate("Map Filter"),
"tooltip": (hoverIdx) => g_MapFilterList.tooltip[hoverIdx] || translate("Select a map filter."),
"labels": () => g_MapFilterList.name,
"ids": () => g_MapFilterList.id,
"default": () => g_MapFilterList.Default,
"defined": () => g_MapFilterList.id.indexOf(g_GameAttributes.mapFilter || "") != -1,
"get": () => g_GameAttributes.mapFilter,
"select": (itemIdx) => {
g_GameAttributes.mapFilter = g_MapFilterList.id[itemIdx];
delete g_GameAttributes.map;
reloadMapList();
},
"autocomplete": 0,
"initOrder": 2
},
"mapSelection": {
"title": () => translate("Select Map"),
"tooltip": (hoverIdx) => g_MapSelectionList.description[hoverIdx] || translate("Select a map to play on."),
"labels": () => g_MapSelectionList.name,
"colors": () => g_MapSelectionList.color,
"ids": () => g_MapSelectionList.file,
"default": () => 0,
"defined": () => g_GameAttributes.map !== undefined,
"get": () => g_GameAttributes.map,
"select": (itemIdx) => {
selectMap(g_MapSelectionList.file[itemIdx]);
},
"autocomplete": 0,
"initOrder": 3
},
"mapSize": {
"title": () => translate("Map Size"),
"tooltip": (hoverIdx) => g_MapSizes.Tooltip[hoverIdx] || translate("Select map size. (Larger sizes may reduce performance.)"),
"labels": () => g_MapSizes.Name,
"ids": () => g_MapSizes.Tiles,
"default": () => g_MapSizes.Default,
"defined": () => g_GameAttributes.settings.Size !== undefined,
"get": () => g_GameAttributes.settings.Size,
"select": (itemIdx) => {
g_GameAttributes.settings.Size = g_MapSizes.Tiles[itemIdx];
},
"hidden": () => g_GameAttributes.mapType != "random",
"autocomplete": 0,
"initOrder": 1000
},
"biome": {
"title": () => translate("Biome"),
"tooltip": (hoverIdx) => g_BiomeList && g_BiomeList.Description && g_BiomeList.Description[hoverIdx] || translate("Select the flora and fauna."),
"labels": () => g_BiomeList ? g_BiomeList.Title : [],
"colors": (itemIdx) => g_BiomeList ? g_BiomeList.Color : [],
"ids": () => g_BiomeList ? g_BiomeList.Id : [],
"default": () => 0,
"defined": () => g_GameAttributes.settings.Biome !== undefined,
"get": () => g_GameAttributes.settings.Biome,
"select": (itemIdx) => {
g_GameAttributes.settings.Biome = g_BiomeList && g_BiomeList.Id[itemIdx];
},
"hidden": () => !g_BiomeList,
"autocomplete": 0,
"initOrder": 1000
},
"numPlayers": {
"title": () => translate("Number of Players"),
"tooltip": (hoverIdx) => translate("Select number of players."),
"labels": () => g_NumPlayersList,
"ids": () => g_NumPlayersList,
"default": () => g_MaxPlayers - 1,
"defined": () => g_GameAttributes.settings.PlayerData !== undefined,
"get": () => g_GameAttributes.settings.PlayerData.length,
"enabled": () => g_GameAttributes.mapType == "random",
"select": (itemIdx) => {
let num = itemIdx + 1;
let pData = g_GameAttributes.settings.PlayerData;
g_GameAttributes.settings.PlayerData =
num > pData.length ?
pData.concat(clone(g_DefaultPlayerData.slice(pData.length, num))) :
pData.slice(0, num);
unassignInvalidPlayers(num);
sanitizePlayerData(g_GameAttributes.settings.PlayerData);
},
"initOrder": 1000
},
"populationCap": {
"title": () => translate("Population Cap"),
"tooltip": (hoverIdx) => {
let popCap = g_PopulationCapacities.Population[hoverIdx];
let players = g_GameAttributes.settings.PlayerData.length;
if (hoverIdx == -1 || popCap * players <= g_PopulationCapacityRecommendation)
return translate("Select population limit.");
return coloredText(
sprintf(translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population."), {
"players": players,
"popCap": popCap
}),
"orange");
},
"labels": () => g_PopulationCapacities.Title,
"ids": () => g_PopulationCapacities.Population,
"default": () => g_PopulationCapacities.Default,
"defined": () => g_GameAttributes.settings.PopulationCap !== undefined,
"get": () => g_GameAttributes.settings.PopulationCap,
"select": (itemIdx) => {
g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[itemIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"startingResources": {
"title": () => translate("Starting Resources"),
"tooltip": (hoverIdx) => {
return hoverIdx >= 0 ?
sprintf(translate("Initial amount of each resource: %(resources)s."), {
"resources": g_StartingResources.Resources[hoverIdx]
}) :
translate("Select the game's starting resources.");
},
"labels": () => g_StartingResources.Title,
"ids": () => g_StartingResources.Resources,
"default": () => g_StartingResources.Default,
"defined": () => g_GameAttributes.settings.StartingResources !== undefined,
"get": () => g_GameAttributes.settings.StartingResources,
"select": (itemIdx) => {
g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[itemIdx];
},
"hidden": () => g_GameAttributes.mapType == "scenario",
"autocomplete": 0,
"initOrder": 1000
},
"ceasefire": {
"title": () => translate("Ceasefire"),
"tooltip": (hoverIdx) => translate("Set time where no attacks are possible."),
"labels": () => g_Ceasefire.Title,
"ids": () => g_Ceasefire.Duration,
"default": () => g_Ceasefire.Default,
"defined": () => g_GameAttributes.settings.Ceasefire !== undefined,
"get": () => g_GameAttributes.settings.Ceasefire,
"select": (itemIdx) => {
g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[itemIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
- "victoryCondition": {
- "title": () => translate("Victory Condition"),
- "tooltip": (hoverIdx) => g_VictoryConditions.Description[hoverIdx] || translate("Select victory condition."),
- "labels": () => g_VictoryConditions.Title,
- "ids": () => g_VictoryConditions.Name,
- "default": () => g_VictoryConditions.Default,
- "defined": () => g_GameAttributes.settings.GameType !== undefined,
- "get": () => g_GameAttributes.settings.GameType,
- "select": (itemIdx) => {
- g_GameAttributes.settings.GameType = g_VictoryConditions.Name[itemIdx];
- g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[itemIdx];
- },
- "enabled": () => g_GameAttributes.mapType != "scenario",
- "autocomplete": 0,
- "initOrder": 1000
- },
"relicCount": {
"title": () => translate("Relic Count"),
"tooltip": (hoverIdx) => translate("Total number of relics spawned on the map. Relic victory is most realistic with only one or two relics. With greater numbers, the relics are important to capture to receive aura bonuses."),
"labels": () => g_RelicCountList,
"ids": () => g_RelicCountList,
"default": () => g_RelicCountList.indexOf(2),
"defined": () => g_GameAttributes.settings.RelicCount !== undefined,
"get": () => g_GameAttributes.settings.RelicCount,
"select": (itemIdx) => {
g_GameAttributes.settings.RelicCount = g_RelicCountList[itemIdx];
},
- "hidden": () => g_GameAttributes.settings.GameType != "capture_the_relic",
+ "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1,
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"relicDuration": {
"title": () => translate("Relic Duration"),
"tooltip": (hoverIdx) => translate("Minutes until the player has achieved Relic Victory."),
"labels": () => g_VictoryDurations.Title,
"ids": () => g_VictoryDurations.Duration,
"default": () => g_VictoryDurations.Default,
"defined": () => g_GameAttributes.settings.RelicDuration !== undefined,
"get": () => g_GameAttributes.settings.RelicDuration,
"select": (itemIdx) => {
g_GameAttributes.settings.RelicDuration = g_VictoryDurations.Duration[itemIdx];
},
- "hidden": () => g_GameAttributes.settings.GameType != "capture_the_relic",
+ "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1,
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"wonderDuration": {
"title": () => translate("Wonder Duration"),
"tooltip": (hoverIdx) => translate("Minutes until the player has achieved Wonder Victory."),
"labels": () => g_VictoryDurations.Title,
"ids": () => g_VictoryDurations.Duration,
"default": () => g_VictoryDurations.Default,
"defined": () => g_GameAttributes.settings.WonderDuration !== undefined,
"get": () => g_GameAttributes.settings.WonderDuration,
"select": (itemIdx) => {
g_GameAttributes.settings.WonderDuration = g_VictoryDurations.Duration[itemIdx];
},
- "hidden": () => g_GameAttributes.settings.GameType != "wonder",
+ "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("wonder") == -1,
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"gameSpeed": {
"title": () => translate("Game Speed"),
"tooltip": (hoverIdx) => translate("Select game speed."),
"labels": () => g_GameSpeeds.Title,
"ids": () => g_GameSpeeds.Speed,
"default": () => g_GameSpeeds.Default,
"defined": () =>
g_GameAttributes.gameSpeed !== undefined &&
g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) != -1,
"get": () => g_GameAttributes.gameSpeed,
"select": (itemIdx) => {
g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[itemIdx];
},
"initOrder": 1000
},
"triggerDifficulty": {
"title": () => translate("Difficulty"),
"tooltip": (hoverIdx) => g_TriggerDifficultyList && g_TriggerDifficultyList.Description[hoverIdx] ||
translate("Select the difficulty of this scenario."),
"labels": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Title : [],
"ids": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Id : [],
"default": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Default : 0,
"defined": () => g_GameAttributes.settings.TriggerDifficulty !== undefined,
"get": () => g_GameAttributes.settings.TriggerDifficulty,
"select": (itemIdx) => {
g_GameAttributes.settings.TriggerDifficulty = g_TriggerDifficultyList && g_TriggerDifficultyList.Id[itemIdx];
},
"hidden": () => !g_TriggerDifficultyList,
"initOrder": 1000
},
};
/**
* These dropdowns provide a setting that is repeated once for each player
* (where playerIdx is starting from 0 for player 1).
*/
var g_PlayerDropdowns = {
"playerAssignment": {
"labels": (playerIdx) => g_PlayerAssignmentList.Name || [],
"colors": (playerIdx) => g_PlayerAssignmentList.Color || [],
"ids": (playerIdx) => g_PlayerAssignmentList.Choice || [],
"default": (playerIdx) => "ai:petra",
"defined": (playerIdx) => playerIdx < g_GameAttributes.settings.PlayerData.length,
"get": (playerIdx) => {
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player == playerIdx + 1)
return "guid:" + guid;
for (let ai of g_Settings.AIDescriptions)
if (g_GameAttributes.settings.PlayerData[playerIdx].AI == ai.id)
return "ai:" + ai.id;
return "unassigned";
},
"select": (selectedIdx, playerIdx) => {
let choice = g_PlayerAssignmentList.Choice[selectedIdx];
if (choice == "unassigned" || choice.startsWith("ai:"))
{
if (g_IsNetworked)
Engine.AssignNetworkPlayer(playerIdx+1, "");
else if (g_PlayerAssignments.local.player == playerIdx+1)
g_PlayerAssignments.local.player = -1;
g_GameAttributes.settings.PlayerData[playerIdx].AI = choice.startsWith("ai:") ? choice.substr(3) : "";
}
else
swapPlayers(choice.substr("guid:".length), playerIdx);
},
"autocomplete": 100,
},
"playerTeam": {
"labels": (playerIdx) => g_PlayerTeamList.label,
"ids": (playerIdx) => g_PlayerTeamList.id,
"default": (playerIdx) => 0,
"defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team !== undefined,
"get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team,
"select": (selectedIdx, playerIdx) => {
g_GameAttributes.settings.PlayerData[playerIdx].Team = selectedIdx - 1;
},
"enabled": () => g_GameAttributes.mapType != "scenario",
},
"playerCiv": {
"tooltip": (hoverIdx, playerIdx) => g_PlayerCivList.tooltip[hoverIdx] || translate("Chose the civilization for this player"),
"labels": (playerIdx) => g_PlayerCivList.name,
"colors": (playerIdx) => g_PlayerCivList.color,
"ids": (playerIdx) => g_PlayerCivList.code,
"default": (playerIdx) => 0,
"defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ !== undefined,
"get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ,
"select": (selectedIdx, playerIdx) => {
g_GameAttributes.settings.PlayerData[playerIdx].Civ = g_PlayerCivList.code[selectedIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"autocomplete": 0,
},
"playerColorPicker": {
"labels": (playerIdx) => g_PlayerColorPickerList.map(color => "■"),
"colors": (playerIdx) => g_PlayerColorPickerList.map(color => rgbToGuiColor(color)),
"ids": (playerIdx) => g_PlayerColorPickerList.map((color, index) => index),
"default": (playerIdx) => playerIdx,
"defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Color !== undefined,
"get": (playerIdx) => g_PlayerColorPickerList.indexOf(g_GameAttributes.settings.PlayerData[playerIdx].Color),
"select": (selectedIdx, playerIdx) => {
let playerData = g_GameAttributes.settings.PlayerData;
// If someone else has that color, give that player the old color
let sameColorPData = playerData.find(pData => sameColor(g_PlayerColorPickerList[selectedIdx], pData.Color));
if (sameColorPData)
sameColorPData.Color = playerData[playerIdx].Color;
playerData[playerIdx].Color = g_PlayerColorPickerList[selectedIdx];
ensureUniquePlayerColors(playerData);
},
"enabled": () => g_GameAttributes.mapType != "scenario",
},
};
/**
* Contains the logic of all boolean gamesettings.
*/
-var g_Checkboxes = {
+var g_Checkboxes = Object.assign(
+ {},
+ g_VictoryConditions.reduce((obj, victoryCondition) => {
+ obj[victoryCondition.Name] = {
+ "title": () => victoryCondition.Title,
+ "tooltip": () => victoryCondition.Description,
+ // Defaults are set in supplementDefault directly from g_VictoryConditions since we use an array
+ "defined": () => true,
+ "get": () => g_GameAttributes.settings.VictoryConditions.indexOf(victoryCondition.Name) != -1,
+ "set": checked => {
+ if (checked)
+ {
+ g_GameAttributes.settings.VictoryConditions.push(victoryCondition.Name);
+ if (victoryCondition.Set)
+ for (let setting in victoryCondition.ChangeWhenChecked)
+ g_Checkboxes[setting].set(victoryCondition.ChangeOnChecked[setting]);
+ }
+ else
+ g_GameAttributes.settings.VictoryConditions = g_GameAttributes.settings.VictoryConditions.filter(victoryConditionName => victoryConditionName != victoryCondition.Name);
+ },
+ "enabled": () =>
+ g_GameAttributes.mapType != "scenario" &&
+ (!victoryCondition.DisabledWhenChecked ||
+ victoryCondition.DisabledWhenChecked.every(victoryConditionName => g_GameAttributes.settings.VictoryConditions.indexOf(victoryConditionName) == -1))
+ };
+ return obj;
+ }, {}),
+ {
"regicideGarrison": {
"title": () => translate("Hero Garrison"),
"tooltip": () => translate("Toggle whether heroes can be garrisoned."),
"default": () => false,
"defined": () => g_GameAttributes.settings.RegicideGarrison !== undefined,
"get": () => g_GameAttributes.settings.RegicideGarrison,
"set": checked => {
g_GameAttributes.settings.RegicideGarrison = checked;
},
- "hidden": () => g_GameAttributes.settings.GameType != "regicide",
+ "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("regicide") == -1,
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"nomad": {
"title": () => translate("Nomad"),
"tooltip": () => translate("In Nomad mode, players start with only few units and have to find a suitable place to build their city. Ceasefire is recommended."),
"default": () => false,
"defined": () => g_GameAttributes.settings.Nomad !== undefined,
"get": () => g_GameAttributes.settings.Nomad,
"set": checked => {
g_GameAttributes.settings.Nomad = checked;
},
"hidden": () => g_GameAttributes.mapType != "random",
"initOrder": 1000
},
"revealMap": {
"title": () =>
// Translation: Make sure to differentiate between the revealed map and explored map settings!
translate("Revealed Map"),
"tooltip":
// Translation: Make sure to differentiate between the revealed map and explored map settings!
() => translate("Toggle revealed map (see everything)."),
"default": () => false,
"defined": () => g_GameAttributes.settings.RevealMap !== undefined,
"get": () => g_GameAttributes.settings.RevealMap,
"set": checked => {
g_GameAttributes.settings.RevealMap = checked;
if (checked)
g_Checkboxes.exploreMap.set(true);
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"exploreMap": {
"title":
// Translation: Make sure to differentiate between the revealed map and explored map settings!
() => translate("Explored Map"),
"tooltip":
// Translation: Make sure to differentiate between the revealed map and explored map settings!
() => translate("Toggle explored map (see initial map)."),
"default": () => false,
"defined": () => g_GameAttributes.settings.ExploreMap !== undefined,
"get": () => g_GameAttributes.settings.ExploreMap,
"set": checked => {
g_GameAttributes.settings.ExploreMap = checked;
},
"enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RevealMap,
"initOrder": 1000
},
"disableTreasures": {
"title": () => translate("Disable Treasures"),
"tooltip": () => translate("Disable all treasures on the map."),
"default": () => false,
"defined": () => g_GameAttributes.settings.DisableTreasures !== undefined,
"get": () => g_GameAttributes.settings.DisableTreasures,
"set": checked => {
g_GameAttributes.settings.DisableTreasures = checked;
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"disableSpies": {
"title": () => translate("Disable Spies"),
"tooltip": () => translate("Disable spies during the game."),
"default": () => false,
"defined": () => g_GameAttributes.settings.DisableSpies !== undefined,
"get": () => g_GameAttributes.settings.DisableSpies,
"set": checked => {
g_GameAttributes.settings.DisableSpies = checked;
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"lockTeams": {
"title": () => translate("Teams Locked"),
"tooltip": () => translate("Toggle locked teams."),
"default": () => Engine.HasXmppClient(),
"defined": () => g_GameAttributes.settings.LockTeams !== undefined,
"get": () => g_GameAttributes.settings.LockTeams,
"set": checked => {
g_GameAttributes.settings.LockTeams = checked;
g_GameAttributes.settings.LastManStanding = false;
},
"enabled": () =>
g_GameAttributes.mapType != "scenario" &&
!g_GameAttributes.settings.RatingEnabled,
"initOrder": 1000
},
"lastManStanding": {
"title": () => translate("Last Man Standing"),
"tooltip": () => translate("Toggle whether the last remaining player or the last remaining set of allies wins."),
"default": () => false,
"defined": () => g_GameAttributes.settings.LastManStanding !== undefined,
"get": () => g_GameAttributes.settings.LastManStanding,
"set": checked => {
g_GameAttributes.settings.LastManStanding = checked;
},
"enabled": () =>
g_GameAttributes.mapType != "scenario" &&
!g_GameAttributes.settings.LockTeams,
"initOrder": 1000
},
"enableCheats": {
"title": () => translate("Cheats"),
"tooltip": () => translate("Toggle the usability of cheats."),
"default": () => !g_IsNetworked,
"hidden": () => !g_IsNetworked,
"defined": () => g_GameAttributes.settings.CheatsEnabled !== undefined,
"get": () => g_GameAttributes.settings.CheatsEnabled,
"set": checked => {
g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked ||
checked && !g_GameAttributes.settings.RatingEnabled;
},
"enabled": () => !g_GameAttributes.settings.RatingEnabled,
"initOrder": 1000
},
"enableRating": {
"title": () => translate("Rated Game"),
"tooltip": () => translate("Toggle if this game will be rated for the leaderboard."),
"default": () => Engine.HasXmppClient(),
"hidden": () => !Engine.HasXmppClient(),
"defined": () => g_GameAttributes.settings.RatingEnabled !== undefined,
"get": () => !!g_GameAttributes.settings.RatingEnabled,
"set": checked => {
g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient() ? checked : undefined;
Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
if (checked)
{
g_Checkboxes.lockTeams.set(true);
g_Checkboxes.enableCheats.set(false);
}
},
"initOrder": 1000
},
-};
+ }
+);
/**
* For setting up arbitrary GUI objects.
*/
var g_MiscControls = {
"chatPanel": {
"hidden": () => {
if (!g_IsNetworked)
return true;
let size = Engine.GetGUIObjectByName("chatPanel").getComputedSize();
return size.right - size.left < g_MinChatWidth;
},
},
"chatInput": {
"tooltip": () => colorizeAutocompleteHotkey(translate("Press %(hotkey)s to autocomplete playernames or settings.")),
},
"cheatWarningText": {
"hidden": () => !g_IsNetworked || !g_GameAttributes.settings.CheatsEnabled,
},
"cancelGame": {
"tooltip": () =>
Engine.HasXmppClient() ?
translate("Return to the lobby.") :
translate("Return to the main menu."),
},
"startGame": {
"caption": () =>
g_IsController ? translate("Start Game!") : g_ReadyData[g_IsReady].caption,
"tooltip": (hoverIdx) =>
!g_IsController ?
g_ReadyData[g_IsReady].tooltip :
!g_IsNetworked || Object.keys(g_PlayerAssignments).every(guid =>
g_PlayerAssignments[guid].status || g_PlayerAssignments[guid].player == -1) ?
translate("Start a new game with the current settings.") :
translate("Start a new game with the current settings (disabled until all players are ready)"),
"enabled": () => !g_IsController ||
Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].status ||
g_PlayerAssignments[guid].player == -1 ||
guid == Engine.GetPlayerGUID() && g_IsController),
"hidden": () =>
!g_PlayerAssignments[Engine.GetPlayerGUID()] ||
g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1 && !g_IsController,
},
"civResetButton": {
"hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
},
"teamResetButton": {
"hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
},
"lobbyButton": {
"onPress": () => function() {
if (Engine.HasXmppClient())
Engine.PushGuiPage("page_lobby.xml", { "dialog": true });
},
"hidden": () => !Engine.HasXmppClient()
},
"spTips": {
"hidden": () => {
let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
let spTips = Engine.GetGUIObjectByName("spTips");
return g_IsNetworked ||
Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true" ||
spTips.size.right > settingsPanel.getComputedSize().left;
}
}
};
/**
* Contains gui elements that are repeated for every player.
*/
var g_PlayerMiscElements = {
"playerBox": {
"size": (playerIdx) => ["0", 32 * playerIdx, "100%", 32 * (playerIdx + 1)].join(" "),
},
"playerName": {
"caption": (playerIdx) => {
let pData = g_GameAttributes.settings.PlayerData[playerIdx];
let assignedGUID = Object.keys(g_PlayerAssignments).find(
guid => g_PlayerAssignments[guid].player == playerIdx + 1);
let name = translate(pData.Name || g_DefaultPlayerData[playerIdx].Name);
if (g_IsNetworked)
name = coloredText(name, g_ReadyData[assignedGUID ? g_PlayerAssignments[assignedGUID].status : 2].color);
return name;
},
},
"playerColor": {
"sprite": (playerIdx) => "color:" + rgbToGuiColor(g_GameAttributes.settings.PlayerData[playerIdx].Color, 100),
},
"playerConfig": {
"hidden": (playerIdx) => !g_GameAttributes.settings.PlayerData[playerIdx].AI,
"onPress": (playerIdx) => function() {
openAIConfig(playerIdx);
},
"tooltip": (playerIdx) => sprintf(translate("Configure AI: %(description)s."), {
"description": translateAISettings(g_GameAttributes.settings.PlayerData[playerIdx])
}),
},
};
/**
* Initializes some globals without touching the GUI.
*
* @param {Object} attribs - context data sent by the lobby / mainmenu
*/
function init(attribs)
{
if (!g_Settings)
{
cancelSetup();
return;
}
if (["offline", "server", "client"].indexOf(attribs.type) == -1)
{
error("Unexpected 'type' in gamesetup init: " + attribs.type);
cancelSetup();
return;
}
g_IsNetworked = attribs.type != "offline";
g_IsController = attribs.type != "client";
g_IsTutorial = !!attribs.tutorial;
g_ServerName = attribs.serverName;
g_ServerPort = attribs.serverPort;
g_StunEndpoint = attribs.stunEndpoint;
if (!g_IsNetworked)
g_PlayerAssignments = {
"local": {
"name": singleplayerName(),
"player": 1
}
};
// Replace empty playername when entering a singleplayermatch for the first time
if (!g_IsNetworked)
saveSettingAndWriteToUserConfig("playername.singleplayer", singleplayerName());
initDefaults();
supplementDefaults();
setTimeout(displayGamestateNotifications, 1000);
Engine.GetGUIObjectByName("civInfoButton").tooltip = sprintf(
translate("%(hotkey_civinfo)s / %(hotkey_structree)s: View History / Structure Tree\nLast opened will be reopened on click."), {
"hotkey_civinfo": colorizeHotkey("%(hotkey)s", "civinfo"),
"hotkey_structree": colorizeHotkey("%(hotkey)s", "structree")
});
}
function initDefaults()
{
// Remove gaia from both arrays
g_DefaultPlayerData = clone(g_Settings.PlayerDefaults.slice(1));
let aiDifficulty = +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty");
let aiBehavior = Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior");
// Don't change the underlying defaults file, as Atlas uses that file too
for (let i in g_DefaultPlayerData)
{
g_DefaultPlayerData[i].Civ = "random";
g_DefaultPlayerData[i].Team = -1;
g_DefaultPlayerData[i].AIDiff = aiDifficulty;
g_DefaultPlayerData[i].AIBehavior = aiBehavior;
}
deepfreeze(g_DefaultPlayerData);
}
/**
* Sets default values for all g_GameAttribute settings which don't have a value set.
*/
function supplementDefaults()
{
+ g_GameAttributes.settings.VictoryConditions = g_GameAttributes.settings.VictoryConditions ||
+ g_VictoryConditions.filter(victoryCondition => !!victoryCondition.Default).map(victoryCondition => victoryCondition.Name);
+
for (let dropdown in g_Dropdowns)
if (!g_Dropdowns[dropdown].defined())
g_Dropdowns[dropdown].select(g_Dropdowns[dropdown].default());
for (let checkbox in g_Checkboxes)
if (!g_Checkboxes[checkbox].defined())
g_Checkboxes[checkbox].set(g_Checkboxes[checkbox].default());
for (let dropdown in g_PlayerDropdowns)
for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i)
if (!isControlArrayElementHidden(i) && !g_PlayerDropdowns[dropdown].defined(i))
g_PlayerDropdowns[dropdown].select(g_PlayerDropdowns[dropdown].default(i), i);
}
/**
* Called after the first tick.
*/
function initGUIObjects()
{
for (let tab in g_SettingsTabsGUI)
g_SettingsTabsGUI[tab].tooltip =
sprintf(translate("Toggle the %(name)s settings tab."), { "name": g_SettingsTabsGUI[tab].label }) +
colorizeHotkey("\n" + translate("Use %(hotkey)s to move a settings tab down."), "tab.next") +
colorizeHotkey("\n" + translate("Use %(hotkey)s to move a settings tab up."), "tab.prev");
// Copy all initOrder values into one object
let initOrder = {};
for (let dropdown in g_Dropdowns)
initOrder[dropdown] = g_Dropdowns[dropdown].initOrder;
for (let checkbox in g_Checkboxes)
initOrder[checkbox] = g_Checkboxes[checkbox].initOrder;
// Sort the object on initOrder so we can init the settings in an arbitrary order
for (let setting of Object.keys(initOrder).sort((a, b) => initOrder[a] - initOrder[b]))
if (g_Dropdowns[setting])
initDropdown(setting);
else if (g_Checkboxes[setting])
initCheckbox(setting);
else
warn('The setting "' + setting + '" is not defined.');
for (let dropdown in g_PlayerDropdowns)
initPlayerDropdowns(dropdown);
let settingTabButtons = Engine.GetGUIObjectByName("settingTabButtons");
let settingTabButtonsSize = settingTabButtons.size;
settingTabButtonsSize.bottom = settingTabButtonsSize.top + g_SettingsTabsGUI.length * (g_TabButtonHeight + g_TabButtonDist);
settingTabButtonsSize.right = g_MiscControls.lobbyButton.hidden() ?
settingTabButtonsSize.right :
Engine.GetGUIObjectByName("lobbyButton").size.left - g_LobbyButtonSpacing;
settingTabButtons.size = settingTabButtonsSize;
let settingTabButtonsBackground = Engine.GetGUIObjectByName("settingTabButtonsBackground");
settingTabButtonsBackground.size = settingTabButtonsSize;
let gameDescription = Engine.GetGUIObjectByName("mapInfoDescriptionFrame");
let gameDescriptionSize = gameDescription.size;
gameDescriptionSize.top = settingTabButtonsSize.bottom + 3;
gameDescription.size = gameDescriptionSize;
placeTabButtons(
g_SettingsTabsGUI,
g_TabButtonHeight,
g_TabButtonDist,
category => {
selectPanel(category == g_TabCategorySelected ? undefined : category);
},
() => {
updateGUIObjects();
Engine.GetGUIObjectByName("settingsPanel").hidden = false;
});
initSPTips();
loadPersistMatchSettings();
updateGameAttributes();
sendRegisterGameStanzaImmediate();
if (g_IsTutorial)
{
launchTutorial();
return;
}
// Don't lift the curtain until the controls are updated the first time
if (!g_IsNetworked)
hideLoadingWindow();
}
/**
* Slide settings panel.
* @param {number} dt - Time in milliseconds since last call.
*/
function updateSettingsPanelPosition(dt)
{
let slideSpeed = Engine.ConfigDB_GetValue("user", "gui.gamesetup.settingsslide") == "true" ? g_SlideSpeed : Infinity;
let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
let rightBorder = Engine.GetGUIObjectByName("settingTabButtons").size.left;
let offset = 0;
if (g_TabCategorySelected === undefined)
{
let maxOffset = rightBorder - settingsPanel.size.left;
if (maxOffset > 0)
offset = Math.min(slideSpeed * dt, maxOffset);
}
else if (rightBorder > settingsPanel.size.right)
offset = Math.min(slideSpeed * dt, rightBorder - settingsPanel.size.right);
else
{
let maxOffset = settingsPanel.size.right - rightBorder;
if (maxOffset > 0)
offset = -Math.min(slideSpeed * dt, maxOffset);
}
let size = settingsPanel.size;
size.left += offset;
size.right += offset;
settingsPanel.size = size;
let settingsBackground = Engine.GetGUIObjectByName("settingsBackground");
let backgroundSize = settingsBackground.size;
backgroundSize.left = size.left;
settingsBackground.size = backgroundSize;
let chatPanel = Engine.GetGUIObjectByName("chatPanel");
let chatSize = chatPanel.size;
chatSize.right = size.left - g_ChatSettingsMargin;
chatPanel.size = chatSize;
chatPanel.hidden = g_MiscControls.chatPanel.hidden();
let spTips = Engine.GetGUIObjectByName("spTips");
spTips.hidden = g_MiscControls.spTips.hidden();
}
function hideLoadingWindow()
{
let loadingWindow = Engine.GetGUIObjectByName("loadingWindow");
if (loadingWindow.hidden)
return;
loadingWindow.hidden = true;
Engine.GetGUIObjectByName("setupWindow").hidden = false;
if (!Engine.GetGUIObjectByName("chatPanel").hidden)
Engine.GetGUIObjectByName("chatInput").focus();
}
/**
* Settings under the settings tabs use a generic name.
* Player settings use custom names.
*/
function getGUIObjectNameFromSetting(setting)
{
let idxOffset = 0;
for (let category of g_SettingsTabsGUI)
{
let idx = category.settings.indexOf(setting);
if (idx != -1)
return [
"setting",
g_Dropdowns[setting] ? "Dropdown" : "Checkbox",
"[" + (idx + idxOffset) + "]"
];
idxOffset += category.settings.length;
}
// Assume there is a GUI object with exactly that setting name
return [setting, "", ""];
}
function initDropdown(name, playerIdx)
{
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
let data = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name];
let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName);
dropdown.list = data.labels(playerIdx).map((label, id) =>
data.colors && data.colors(playerIdx) ?
coloredText(label, data.colors(playerIdx)[id]) :
label);
dropdown.list_data = data.ids(playerIdx);
dropdown.onSelectionChange = function() {
if (!g_IsController ||
g_IsInGuiUpdate ||
!this.list_data[this.selected] ||
data.hidden && data.hidden(playerIdx) ||
data.enabled && !data.enabled(playerIdx))
return;
data.select(this.selected, playerIdx);
supplementDefaults();
updateGameAttributes();
};
if (data.tooltip)
dropdown.onHoverChange = function() {
this.tooltip = data.tooltip(this.hovered, playerIdx);
};
}
function initPlayerDropdowns(name)
{
for (let i = 0; i < g_MaxPlayers; ++i)
initDropdown(name, i);
}
function initCheckbox(name)
{
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
Engine.GetGUIObjectByName(guiName + guiType + guiIdx).onPress = function() {
let obj = g_Checkboxes[name];
if (!g_IsController ||
g_IsInGuiUpdate ||
obj.enabled && !obj.enabled() ||
obj.hidden && obj.hidden())
return;
obj.set(this.checked);
supplementDefaults();
updateGameAttributes();
};
}
function initSPTips()
{
if (g_IsNetworked || Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true")
return;
Engine.GetGUIObjectByName("spTips").hidden = false;
Engine.GetGUIObjectByName("displaySPTips").checked = true;
Engine.GetGUIObjectByName("aiTips").caption = Engine.TranslateLines(Engine.ReadFile("gui/gamesetup/ai.txt"));
}
/**
* Distribute the currently visible settings over the settings panel.
* First calculate the number of columns required, then place the objects.
*/
function distributeSettings()
{
let setupWindowSize = Engine.GetGUIObjectByName("setupWindow").getComputedSize();
let columnWidth = Math.min(
g_MaxColumnWidth,
(setupWindowSize.right - setupWindowSize.left + Engine.GetGUIObjectByName("settingTabButtons").size.left) / 2);
let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
let actualSettingsPanelSize = settingsPanel.getComputedSize();
let maxPerColumn = Math.floor((actualSettingsPanelSize.bottom - actualSettingsPanelSize.top) / g_SettingHeight);
let childCount = settingsPanel.children.filter(child => !child.hidden).length;
let perColumn = childCount / Math.ceil(childCount / maxPerColumn);
let yPos = g_SettingDist;
let column = 0;
let thisColumn = 0;
let settingsPanelSize = settingsPanel.size;
for (let child of settingsPanel.children)
{
if (child.hidden)
continue;
if (thisColumn >= perColumn)
{
yPos = g_SettingDist;
++column;
thisColumn = 0;
}
let childSize = child.size;
child.size = new GUISize(
column * columnWidth,
yPos,
column * columnWidth + columnWidth - 10,
yPos + g_SettingHeight - g_SettingDist);
yPos += g_SettingHeight;
++thisColumn;
}
settingsPanelSize.right = settingsPanelSize.left + (column + 1) * columnWidth;
settingsPanel.size = settingsPanelSize;
}
/**
* Called when the client disconnects.
* The other cases from NetClient should never occur in the gamesetup.
*/
function handleNetStatusMessage(message)
{
if (message.status != "disconnected")
{
error("Unrecognised netstatus type " + message.status);
return;
}
cancelSetup();
reportDisconnect(message.reason, true);
}
/**
* Called whenever a client clicks on ready (or not ready).
*/
function handleReadyMessage(message)
{
--g_ReadyChanged;
if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1)
addChatMessage({
"type": "ready",
"status": message.status,
"guid": message.guid
});
g_PlayerAssignments[message.guid].status = message.status;
updateGUIObjects();
}
/**
* Called after every player is ready and the host decided to finally start the game.
*/
function handleGamestartMessage(message)
{
// Immediately inform the lobby server instead of waiting for the load to finish
if (g_IsController && Engine.HasXmppClient())
{
sendRegisterGameStanzaImmediate();
let clients = formatClientsForStanza();
Engine.SendChangeStateGame(clients.connectedPlayers, clients.list);
}
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"isNetworked": g_IsNetworked,
"playerAssignments": g_PlayerAssignments,
"isController": g_IsController
});
}
/**
* Called whenever the host changed any setting.
*/
function handleGamesetupMessage(message)
{
if (!message.data)
return;
g_GameAttributes = message.data;
if (!!g_GameAttributes.settings.RatingEnabled)
{
g_GameAttributes.settings.CheatsEnabled = false;
g_GameAttributes.settings.LockTeams = true;
g_GameAttributes.settings.LastManStanding = false;
}
Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
resetReadyData();
updateGUIObjects();
hideLoadingWindow();
}
/**
* Called whenever a client joins/leaves or any gamesetting is changed.
*/
function handlePlayerAssignmentMessage(message)
{
let playerChange = false;
for (let guid in message.newAssignments)
if (!g_PlayerAssignments[guid])
{
onClientJoin(guid, message.newAssignments);
playerChange = true;
}
for (let guid in g_PlayerAssignments)
if (!message.newAssignments[guid])
{
onClientLeave(guid);
playerChange = true;
}
g_PlayerAssignments = message.newAssignments;
sanitizePlayerData(g_GameAttributes.settings.PlayerData);
updateGUIObjects();
if (playerChange)
sendRegisterGameStanzaImmediate();
else
sendRegisterGameStanza();
}
function onClientJoin(newGUID, newAssignments)
{
let playername = newAssignments[newGUID].name;
addChatMessage({
"type": "connect",
"guid": newGUID,
"username": playername
});
let isRejoiningPlayer = newAssignments[newGUID].player != -1;
// Assign the client (or only buddies if prefered) to an unused playerslot and rejoining players to their old slot
if (!isRejoiningPlayer && playername != newAssignments[Engine.GetPlayerGUID()].name)
{
let assignOption = Engine.ConfigDB_GetValue("user", "gui.gamesetup.assignplayers");
if (assignOption == "disabled" ||
assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(playername).nick) == -1)
return;
}
let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v, i) =>
Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i + 1)
);
// Client is not and cannot become assigned as player
if (!isRejoiningPlayer && freeSlot == -1)
return;
// Assign the joining client to the free slot
if (g_IsController && !isRejoiningPlayer)
Engine.AssignNetworkPlayer(freeSlot + 1, newGUID);
resetReadyData();
}
function onClientLeave(guid)
{
addChatMessage({
"type": "disconnect",
"guid": guid
});
if (g_PlayerAssignments[guid].player != -1)
resetReadyData();
}
/**
* Doesn't translate, so that lobby clients can do that locally
* (even if they don't have that map).
*/
function getMapDisplayName(map)
{
if (map == "random")
return map;
let mapData = loadMapData(map);
if (!mapData || !mapData.settings || !mapData.settings.Name)
return map;
return mapData.settings.Name;
}
function getMapPreview(map)
{
let mapData = loadMapData(map);
if (!mapData || !mapData.settings || !mapData.settings.Preview)
return "nopreview.png";
return mapData.settings.Preview;
}
/**
* Filter maps with filterFunc and by chosen map type.
*
* @param {function} filterFunc - Filter function that should be applied.
* @return {Array} the maps that match the filterFunc and the chosen map type.
*/
function getFilteredMaps(filterFunc)
{
if (!g_MapPath[g_GameAttributes.mapType])
{
error("Unexpected map type: " + g_GameAttributes.mapType);
return [];
}
let maps = [];
// TODO: Should verify these are valid maps before adding to list
for (let mapFile of listFiles(g_GameAttributes.mapPath, g_GameAttributes.mapType == "random" ? ".json" : ".xml", false))
{
if (mapFile.startsWith("_"))
continue;
let file = g_GameAttributes.mapPath + mapFile;
let mapData = loadMapData(file);
if (!mapData.settings || filterFunc && !filterFunc(mapData.settings.Keywords || []))
continue;
maps.push({
"file": file,
"name": translate(getMapDisplayName(file)),
"color": g_ColorRegular,
"description": translate(mapData.settings.Description)
});
}
return maps;
}
/**
* Initialize the dropdown containing all map filters for the selected maptype.
*/
function reloadMapFilterList()
{
g_MapFilterList = prepareForDropdown(g_MapFilters.filter(
mapFilter => getFilteredMaps(mapFilter.filter).length
));
initDropdown("mapFilter");
reloadMapList();
}
/**
* Initialize the dropdown containing all maps for the selected maptype and mapfilter.
*/
function reloadMapList()
{
let filterID = g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter);
let filterFunc = g_MapFilterList.filter[filterID];
let mapList = getFilteredMaps(filterFunc).sort(sortNameIgnoreCase);
if (g_GameAttributes.mapType == "random")
mapList.unshift({
"file": "random",
"name": translateWithContext("map selection", "Random"),
"color": g_ColorRandom,
"description": translate("Pick any of the given maps at random.")
});
g_MapSelectionList = prepareForDropdown(mapList);
initDropdown("mapSelection");
}
/**
* Initialize the dropdowns specific to each map.
*/
function reloadMapSpecific()
{
reloadBiomeList();
reloadTriggerDifficulties();
}
function reloadBiomeList()
{
let biomeList;
if (g_GameAttributes.mapType == "random" && g_GameAttributes.settings.SupportedBiomes)
{
if (typeof g_GameAttributes.settings.SupportedBiomes == "string")
biomeList = g_Settings.Biomes.filter(biome => biome.Id.startsWith(g_GameAttributes.settings.SupportedBiomes));
else
biomeList = g_Settings.Biomes.filter(
biome => g_GameAttributes.settings.SupportedBiomes.indexOf(biome.Id) != -1);
}
g_BiomeList = biomeList && prepareForDropdown(
[{
"Id": "random",
"Title": translateWithContext("biome", "Random"),
"Description": translate("Pick a biome at random."),
"Color": g_ColorRandom
}].concat(biomeList.map(biome => ({
"Id": biome.Id,
"Title": biome.Title,
"Description": biome.Description,
"Color": g_ColorRegular
}))));
initDropdown("biome");
updateGUIDropdown("biome");
}
function reloadTriggerDifficulties()
{
g_TriggerDifficultyList = undefined;
if (!g_GameAttributes.settings.SupportedTriggerDifficulties)
return;
let triggerDifficultyList;
if (g_GameAttributes.settings.SupportedTriggerDifficulties.Values === true)
triggerDifficultyList = g_Settings.TriggerDifficulties;
else
{
triggerDifficultyList = g_Settings.TriggerDifficulties.filter(
diff => g_GameAttributes.settings.SupportedTriggerDifficulties.Values.indexOf(diff.Name) != -1);
if (!triggerDifficultyList.length)
return;
}
g_TriggerDifficultyList = prepareForDropdown(
triggerDifficultyList.map(diff => ({
"Id": diff.Difficulty,
"Title": diff.Title,
"Description": diff.Tooltip,
"Default": diff.Name == g_GameAttributes.settings.SupportedTriggerDifficulties.Default
})));
initDropdown("triggerDifficulty");
updateGUIDropdown("triggerDifficulty");
}
function reloadGameSpeedChoices()
{
g_GameSpeeds = getGameSpeedChoices(Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player == -1));
initDropdown("gameSpeed");
supplementDefaults();
}
function loadMapData(name)
{
if (!name || !g_MapPath[g_GameAttributes.mapType])
return undefined;
if (name == "random")
return { "settings": { "Name": "", "Description": "" } };
if (!g_MapData[name])
g_MapData[name] = g_GameAttributes.mapType == "random" ?
Engine.ReadJSONFile(name + ".json") :
Engine.LoadMapSettings(name);
return g_MapData[name];
}
/**
* Sets the gameattributes the way they were the last time the user left the gamesetup.
*/
function loadPersistMatchSettings()
{
if (!g_IsController || Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true" || g_IsTutorial)
return;
let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP;
if (!Engine.FileExists(settingsFile))
return;
let attrs = Engine.ReadJSONFile(settingsFile);
if (!attrs || !attrs.settings)
return;
g_IsInGuiUpdate = true;
let mapName = attrs.map || "";
let mapSettings = attrs.settings;
g_GameAttributes = attrs;
if (!g_IsNetworked)
mapSettings.CheatsEnabled = true;
// Replace unselectable civs with random civ
let playerData = mapSettings.PlayerData;
if (playerData && g_GameAttributes.mapType != "scenario")
for (let i in playerData)
if (!g_CivData[playerData[i].Civ] || !g_CivData[playerData[i].Civ].SelectableInGameSetup)
playerData[i].Civ = "random";
// Apply map settings
let newMapData = loadMapData(mapName);
if (newMapData && newMapData.settings)
{
for (let prop in newMapData.settings)
mapSettings[prop] = newMapData.settings[prop];
if (playerData)
mapSettings.PlayerData = playerData;
}
if (mapSettings.PlayerData)
sanitizePlayerData(mapSettings.PlayerData);
// Reload, as the maptype or mapfilter might have changed
reloadMapFilterList();
reloadMapSpecific();
g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient();
Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled);
supplementDefaults();
g_IsInGuiUpdate = false;
}
function savePersistMatchSettings()
{
if (g_IsTutorial)
return;
let attributes = Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ? g_GameAttributes : {};
Engine.WriteJSONFile(g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP, attributes);
}
function sanitizePlayerData(playerData)
{
// Remove gaia
if (playerData.length && !playerData[0])
playerData.shift();
playerData.forEach((pData, index) => {
// Use defaults if the map doesn't specify a value
for (let prop in g_DefaultPlayerData[index])
if (!(prop in pData))
pData[prop] = clone(g_DefaultPlayerData[index][prop]);
// Replace colors with the best matching color of PlayerDefaults
if (g_GameAttributes.mapType != "scenario")
{
let colorDistances = g_PlayerColorPickerList.map(color => colorDistance(color, pData.Color));
let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance)));
pData.Color = g_PlayerColorPickerList.find(color => colorDistance(color, pData.Color) == smallestDistance);
}
// If there is a player in that slot, then there can't be an AI
if (Object.keys(g_PlayerAssignments).some(guid => g_PlayerAssignments[guid].player == index + 1))
pData.AI = "";
});
ensureUniquePlayerColors(playerData);
}
function cancelSetup()
{
if (g_IsController)
savePersistMatchSettings();
Engine.DisconnectNetworkGame();
if (Engine.HasXmppClient())
{
Engine.LobbySetPlayerPresence("available");
if (g_IsController)
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false });
}
else
Engine.SwitchGuiPage("page_pregame.xml");
}
/**
* Can't init the GUI before the first tick.
* Process netmessages afterwards.
*/
function onTick()
{
if (!g_Settings)
return;
// First tick happens before first render, so don't load yet
if (g_LoadingState == 0)
++g_LoadingState;
else if (g_LoadingState == 1)
{
initGUIObjects();
++g_LoadingState;
}
else if (g_LoadingState == 2)
handleNetMessages();
updateTimers();
let now = Date.now();
let tickLength = now - g_LastTickTime;
g_LastTickTime = now;
updateSettingsPanelPosition(tickLength);
}
/**
* Handles all pending messages sent by the net client.
*/
function handleNetMessages()
{
while (g_IsNetworked)
{
let message = Engine.PollNetworkClient();
if (!message)
break;
log("Net message: " + uneval(message));
if (g_NetMessageTypes[message.type])
g_NetMessageTypes[message.type](message);
else
error("Unrecognised net message type " + message.type);
}
}
/**
* Called when the map or the number of players changes.
*/
function unassignInvalidPlayers(maxPlayers)
{
if (g_IsNetworked)
// Remove invalid playerIDs from the servers playerassignments copy
for (let playerID = +maxPlayers + 1; playerID <= g_MaxPlayers; ++playerID)
Engine.AssignNetworkPlayer(playerID, "");
else if (g_PlayerAssignments.local.player > maxPlayers)
g_PlayerAssignments.local.player = -1;
}
function ensureUniquePlayerColors(playerData)
{
for (let i = playerData.length - 1; i >= 0; --i)
// If someone else has that color, assign an unused color
if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color)))
playerData[i].Color = g_PlayerColorPickerList.find(color => playerData.every(pData => !sameColor(color, pData.Color)));
}
function selectMap(name)
{
// Reset some map specific properties which are not necessarily redefined on each map
for (let prop of ["TriggerScripts", "CircularMap", "Garrison", "DisabledTemplates", "Biome", "SupportedBiomes", "SupportedTriggerDifficulties", "TriggerDifficulty"])
g_GameAttributes.settings[prop] = undefined;
let mapData = loadMapData(name);
let mapSettings = mapData && mapData.settings ? clone(mapData.settings) : {};
if (g_GameAttributes.mapType != "random")
- {
delete g_GameAttributes.settings.Nomad;
- let victoryIdx = g_VictoryConditions.Name.indexOf(mapSettings.GameType || "") != -1 ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
- g_GameAttributes.settings.GameType = g_VictoryConditions.Name[victoryIdx];
- g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[victoryIdx];
- }
-
if (g_GameAttributes.mapType == "scenario")
{
delete g_GameAttributes.settings.RelicDuration;
delete g_GameAttributes.settings.WonderDuration;
delete g_GameAttributes.settings.LastManStanding;
delete g_GameAttributes.settings.RegicideGarrison;
}
if (mapSettings.PlayerData)
sanitizePlayerData(mapSettings.PlayerData);
// Copy any new settings
g_GameAttributes.map = name;
g_GameAttributes.script = mapSettings.Script;
if (g_GameAttributes.map !== "random")
for (let prop in mapSettings)
g_GameAttributes.settings[prop] = mapSettings[prop];
reloadMapSpecific();
unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length);
supplementDefaults();
}
function isControlArrayElementHidden(playerIdx)
{
return playerIdx !== undefined && playerIdx >= g_GameAttributes.settings.PlayerData.length;
}
/**
* @param playerIdx - Only specified for dropdown arrays.
*/
function updateGUIDropdown(name, playerIdx = undefined)
{
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName);
let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx + idxName);
let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx + idxName);
let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx + idxName);
if (guiType == "Dropdown")
Engine.GetGUIObjectByName(guiName + "Checkbox" + guiIdx).hidden = true;
let indexHidden = isControlArrayElementHidden(playerIdx);
let obj = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name];
let hidden = indexHidden || obj.hidden && obj.hidden(playerIdx);
let selected = hidden ? -1 : dropdown.list_data.indexOf(String(obj.get(playerIdx)));
let enabled = !indexHidden && (!obj.enabled || obj.enabled(playerIdx));
dropdown.enabled = g_IsController && enabled;
dropdown.hidden = !g_IsController || !enabled || hidden;
dropdown.selected = selected;
dropdown.tooltip = !indexHidden && obj.tooltip ? obj.tooltip(-1, playerIdx) : "";
if (frame)
frame.hidden = hidden;
if (title && obj.title && !indexHidden)
title.caption = sprintf(translateWithContext("Title for specific setting", "%(setting)s:"), { "setting": obj.title(playerIdx) });
if (label && !indexHidden)
{
label.hidden = g_IsController && enabled || hidden;
label.caption = selected == -1 ? translateWithContext("settings value", "Unknown") : dropdown.list[selected];
}
}
/**
* Not used for the player assignments, so playerCheckboxes are not implemented,
* hence no index.
*/
function updateGUICheckbox(name)
{
let obj = g_Checkboxes[name];
let checked = obj.get();
let hidden = obj.hidden && obj.hidden();
let enabled = !obj.enabled || obj.enabled();
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
let checkbox = Engine.GetGUIObjectByName(guiName + guiType + guiIdx);
let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx);
let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx);
let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx);
if (guiType == "Checkbox")
Engine.GetGUIObjectByName(guiName + "Dropdown" + guiIdx).hidden = true;
checkbox.checked = checked;
checkbox.enabled = g_IsController && enabled;
checkbox.hidden = hidden || !g_IsController;
checkbox.tooltip = obj.tooltip ? obj.tooltip() : "";
label.caption = checked ? translate("Yes") : translate("No");
label.hidden = hidden || g_IsController;
if (frame)
frame.hidden = hidden;
if (title && obj.title)
title.caption = sprintf(translate("%(setting)s:"), { "setting": obj.title() });
}
function updateGUIMiscControl(name, playerIdx)
{
let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
let obj = (playerIdx === undefined ? g_MiscControls : g_PlayerMiscElements)[name];
let control = Engine.GetGUIObjectByName(name + idxName);
if (!control)
warn("No GUI object with name '" + name + "'");
let hide = isControlArrayElementHidden(playerIdx);
control.hidden = hide;
if (hide)
return;
for (let property in obj)
control[property] = obj[property](playerIdx);
}
function launchGame()
{
if (!g_IsController)
{
error("Only host can start game");
return;
}
if (!g_GameAttributes.map)
return;
savePersistMatchSettings();
// Select random map
if (g_GameAttributes.map == "random")
- {
- let victoryScriptsSelected = g_GameAttributes.settings.VictoryScripts;
- let gameTypeSelected = g_GameAttributes.settings.GameType;
selectMap(pickRandom(g_Dropdowns.mapSelection.ids().slice(1)));
- g_GameAttributes.settings.VictoryScripts = victoryScriptsSelected;
- g_GameAttributes.settings.GameType = gameTypeSelected;
- }
if (g_GameAttributes.settings.Biome == "random")
g_GameAttributes.settings.Biome = pickRandom(
typeof g_GameAttributes.settings.SupportedBiomes == "string" ?
g_BiomeList.Id.slice(1).filter(biomeID => biomeID.startsWith(g_GameAttributes.settings.SupportedBiomes)) :
g_GameAttributes.settings.SupportedBiomes);
+ g_GameAttributes.settings.VictoryScripts = g_GameAttributes.settings.VictoryConditions.reduce(
+ (scripts, victoryConditionName) => scripts.concat(g_VictoryConditions[g_VictoryConditions.map(data =>
+ data.Name).indexOf(victoryConditionName)].Scripts.filter(script => scripts.indexOf(script) == -1)),
+ []);
+
g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []);
// Prevent reseting the readystate
g_GameStarted = true;
g_GameAttributes.settings.mapType = g_GameAttributes.mapType;
// Get a unique array of selectable cultures
let cultures = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_CivData[civ].Culture);
cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index);
// Determine random civs and botnames
for (let i in g_GameAttributes.settings.PlayerData)
{
// Pick a random civ of a random culture
let chosenCiv = g_GameAttributes.settings.PlayerData[i].Civ || "random";
if (chosenCiv == "random")
{
let culture = pickRandom(cultures);
chosenCiv = pickRandom(Object.keys(g_CivData).filter(civ => g_CivData[civ].Culture == culture));
}
g_GameAttributes.settings.PlayerData[i].Civ = chosenCiv;
// Pick one of the available botnames for the chosen civ
if (g_GameAttributes.mapType === "scenario" || !g_GameAttributes.settings.PlayerData[i].AI)
continue;
let chosenName = pickRandom(g_CivData[chosenCiv].AINames);
if (!g_IsNetworked)
chosenName = translate(chosenName);
// Count how many players use the chosenName
let usedName = g_GameAttributes.settings.PlayerData.filter(pData => pData.Name && pData.Name.indexOf(chosenName) !== -1).length;
g_GameAttributes.settings.PlayerData[i].Name = !usedName ? chosenName :
sprintf(translate("%(playerName)s %(romanNumber)s"), {
"playerName": chosenName,
"romanNumber": g_RomanNumbers[usedName+1]
});
}
// Copy playernames for the purpose of replays
for (let guid in g_PlayerAssignments)
{
let player = g_PlayerAssignments[guid];
if (player.player > 0) // not observer or GAIA
g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name;
}
// Seed used for both map generation and simulation
g_GameAttributes.settings.Seed = randIntExclusive(0, Math.pow(2, 32));
g_GameAttributes.settings.AISeed = randIntExclusive(0, Math.pow(2, 32));
// Used for identifying rated game reports for the lobby
g_GameAttributes.matchID = Engine.GetMatchID();
if (g_IsNetworked)
{
Engine.SetNetworkGameAttributes(g_GameAttributes);
Engine.StartNetworkGame();
}
else
{
// Find the player ID which the user has been assigned to
let playerID = -1;
for (let i in g_GameAttributes.settings.PlayerData)
{
let assignBox = Engine.GetGUIObjectByName("playerAssignment[" + i + "]");
if (assignBox.list_data[assignBox.selected] == "guid:local")
playerID = +i + 1;
}
Engine.StartGame(g_GameAttributes, playerID);
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"isNetworked": g_IsNetworked,
"playerAssignments": g_PlayerAssignments
});
}
}
function launchTutorial()
{
g_GameAttributes.mapType = "scenario";
selectMap("maps/tutorials/starting_economy_walkthrough");
launchGame();
}
/**
* Don't set any attributes here, just show the changes in the GUI.
*
* Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously.
*/
function updateGUIObjects()
{
g_IsInGuiUpdate = true;
reloadMapFilterList();
reloadMapSpecific();
reloadGameSpeedChoices();
reloadPlayerAssignmentChoices();
// Hide exceeding dropdowns and checkboxes
for (let setting of Engine.GetGUIObjectByName("settingsPanel").children)
setting.hidden = true;
// Show the relevant ones
if (g_TabCategorySelected !== undefined)
{
for (let name in g_Dropdowns)
if (g_SettingsTabsGUI[g_TabCategorySelected].settings.indexOf(name) != -1)
updateGUIDropdown(name);
for (let name in g_Checkboxes)
if (g_SettingsTabsGUI[g_TabCategorySelected].settings.indexOf(name) != -1)
updateGUICheckbox(name);
}
for (let i = 0; i < g_MaxPlayers; ++i)
{
for (let name in g_PlayerDropdowns)
updateGUIDropdown(name, i);
for (let name in g_PlayerMiscElements)
updateGUIMiscControl(name, i);
}
for (let name in g_MiscControls)
updateGUIMiscControl(name);
updateGameDescription();
distributeSettings();
rightAlignCancelButton();
updateAutocompleteEntries();
g_IsInGuiUpdate = false;
// Refresh AI config page
if (g_LastViewedAIPlayer != -1)
{
Engine.PopGuiPage();
openAIConfig(g_LastViewedAIPlayer);
}
}
function rightAlignCancelButton()
{
let offset = 10;
let startGame = Engine.GetGUIObjectByName("startGame");
let right = startGame.hidden ? startGame.size.right : startGame.size.left - offset;
let cancelGame = Engine.GetGUIObjectByName("cancelGame");
let cancelGameSize = cancelGame.size;
let buttonWidth = cancelGameSize.right - cancelGameSize.left;
cancelGameSize.right = right;
right -= buttonWidth;
for (let element of ["cheatWarningText", "onscreenToolTip"])
{
let elementSize = Engine.GetGUIObjectByName(element).size;
elementSize.right = right - (cancelGameSize.left - elementSize.right);
Engine.GetGUIObjectByName(element).size = elementSize;
}
cancelGameSize.left = right;
cancelGame.size = cancelGameSize;
}
function updateGameDescription()
{
setMapPreviewImage("mapPreview", getMapPreview(g_GameAttributes.map));
Engine.GetGUIObjectByName("mapInfoName").caption =
translateMapTitle(getMapDisplayName(g_GameAttributes.map));
Engine.GetGUIObjectByName("mapInfoDescription").caption = getGameDescription();
}
/**
* Broadcast the changed settings to all clients and the lobbybot.
*/
function updateGameAttributes()
{
if (g_IsInGuiUpdate || !g_IsController)
return;
if (g_IsNetworked)
{
Engine.SetNetworkGameAttributes(g_GameAttributes);
if (g_LoadingState >= 2)
sendRegisterGameStanza();
resetReadyData();
}
else
updateGUIObjects();
}
function openAIConfig(playerSlot)
{
g_LastViewedAIPlayer = playerSlot;
Engine.PushGuiPage("page_aiconfig.xml", {
"callback": "AIConfigCallback",
"isController": g_IsController,
"playerSlot": playerSlot,
"id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
"difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff,
"behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior
});
}
/**
* Called after closing the dialog.
*/
function AIConfigCallback(ai)
{
g_LastViewedAIPlayer = -1;
if (!ai.save || !g_IsController)
return;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior;
updateGameAttributes();
}
function reloadPlayerAssignmentChoices()
{
let playerChoices = sortGUIDsByPlayerID().map(guid => ({
"Choice": "guid:" + guid,
"Color": g_PlayerAssignments[guid].player == -1 ? g_PlayerAssignmentColors.observer : g_PlayerAssignmentColors.player,
"Name": g_PlayerAssignments[guid].name
}));
// Only display hidden AIs if the map preselects them
let aiChoices = g_Settings.AIDescriptions
.filter(ai => !ai.data.hidden || g_GameAttributes.settings.PlayerData.some(pData => pData.AI == ai.id))
.map(ai => ({
"Choice": "ai:" + ai.id,
"Name": sprintf(translate("AI: %(ai)s"), {
"ai": translate(ai.data.name)
}),
"Color": g_PlayerAssignmentColors.AI
}));
let unassignedSlot = [{
"Choice": "unassigned",
"Name": translate("Unassigned"),
"Color": g_PlayerAssignmentColors.unassigned
}];
g_PlayerAssignmentList = prepareForDropdown(playerChoices.concat(aiChoices).concat(unassignedSlot));
initPlayerDropdowns("playerAssignment");
}
function swapPlayers(guidToSwap, newSlot)
{
// Player slots are indexed from 0 as Gaia is omitted.
let newPlayerID = newSlot + 1;
let playerID = g_PlayerAssignments[guidToSwap].player;
// Attempt to swap the player or AI occupying the target slot,
// if any, into the slot this player is currently in.
if (playerID != -1)
{
for (let guid in g_PlayerAssignments)
{
// Move the player in the destination slot into the current slot.
if (g_PlayerAssignments[guid].player != newPlayerID)
continue;
if (g_IsNetworked)
Engine.AssignNetworkPlayer(playerID, guid);
else
g_PlayerAssignments[guid].player = playerID;
break;
}
// Transfer the AI from the target slot to the current slot.
g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI;
g_GameAttributes.settings.PlayerData[playerID - 1].AIDiff = g_GameAttributes.settings.PlayerData[newSlot].AIDiff;
g_GameAttributes.settings.PlayerData[playerID - 1].AIBehavior = g_GameAttributes.settings.PlayerData[newSlot].AIBehavior;
// Swap civilizations and colors if they aren't fixed
if (g_GameAttributes.mapType != "scenario")
{
[g_GameAttributes.settings.PlayerData[playerID - 1].Civ, g_GameAttributes.settings.PlayerData[newSlot].Civ] =
[g_GameAttributes.settings.PlayerData[newSlot].Civ, g_GameAttributes.settings.PlayerData[playerID - 1].Civ];
[g_GameAttributes.settings.PlayerData[playerID - 1].Color, g_GameAttributes.settings.PlayerData[newSlot].Color] =
[g_GameAttributes.settings.PlayerData[newSlot].Color, g_GameAttributes.settings.PlayerData[playerID - 1].Color];
}
}
if (g_IsNetworked)
Engine.AssignNetworkPlayer(newPlayerID, guidToSwap);
else
g_PlayerAssignments[guidToSwap].player = newPlayerID;
g_GameAttributes.settings.PlayerData[newSlot].AI = "";
}
function submitChatInput()
{
let chatInput = Engine.GetGUIObjectByName("chatInput");
let text = chatInput.caption;
if (!text.length)
return;
chatInput.caption = "";
if (!executeNetworkCommand(text))
Engine.SendNetworkChat(text);
chatInput.focus();
}
function senderFont(text)
{
return '[font="' + g_SenderFont + '"]' + text + '[/font]';
}
function systemMessage(message)
{
return senderFont(sprintf(translate("== %(message)s"), { "message": message }));
}
function colorizePlayernameByGUID(guid, username = "")
{
// TODO: Maybe the host should have the moderator-prefix?
if (!username)
username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player");
let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
let color = g_ColorRegular;
if (playerID > 0)
{
color = g_GameAttributes.settings.PlayerData[playerID - 1].Color;
// Enlighten playercolor to improve readability
let [h, s, l] = rgbToHsl(color.r, color.g, color.b);
let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l));
color = rgbToGuiColor({ "r": r, "g": g, "b": b });
}
return coloredText(username, color);
}
function addChatMessage(msg)
{
if (!g_FormatChatMessage[msg.type])
return;
if (msg.type == "chat")
{
let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name;
if (userName != g_PlayerAssignments[msg.guid].name &&
msg.text.toLowerCase().indexOf(splitRatingFromNick(userName).nick.toLowerCase()) != -1)
soundNotification("nick");
}
let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || "");
let text = g_FormatChatMessage[msg.type](msg, user);
if (!text)
return;
if (Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true")
text = sprintf(translate("%(time)s %(message)s"), {
"time": sprintf(translate("\\[%(time)s]"), {
"time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), translate("HH:mm"))
}),
"message": text
});
g_ChatMessages.push(text);
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}
function resetCivilizations()
{
for (let i in g_GameAttributes.settings.PlayerData)
g_GameAttributes.settings.PlayerData[i].Civ = "random";
updateGameAttributes();
}
function resetTeams()
{
for (let i in g_GameAttributes.settings.PlayerData)
g_GameAttributes.settings.PlayerData[i].Team = -1;
updateGameAttributes();
}
function toggleReady()
{
setReady((g_IsReady + 1) % 3, true);
}
function setReady(ready, sendMessage)
{
g_IsReady = ready;
if (sendMessage)
Engine.SendNetworkReady(g_IsReady);
updateGUIObjects();
}
function resetReadyData()
{
if (g_GameStarted)
return;
if (g_ReadyChanged < 1)
addChatMessage({ "type": "settings" });
else if (g_ReadyChanged == 2 && !g_ReadyInit)
return; // duplicate calls on init
else
g_ReadyInit = false;
g_ReadyChanged = 2;
if (!g_IsNetworked)
g_IsReady = 2;
else if (g_IsController)
{
Engine.ClearAllPlayerReady();
setReady(2, true);
}
else if (g_IsReady != 2)
setReady(0, false);
}
/**
* Send a list of playernames and distinct between players and observers.
* Don't send teams, AIs or anything else until the game was started.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
function formatClientsForStanza()
{
let connectedPlayers = 0;
let playerData = [];
for (let guid in g_PlayerAssignments)
{
let pData = { "Name": g_PlayerAssignments[guid].name };
if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
++connectedPlayers;
else
pData.Team = "observer";
playerData.push(pData);
}
return {
"list": playerDataToStringifiedTeamList(playerData),
"connectedPlayers": connectedPlayers
};
}
/**
* Send the relevant gamesettings to the lobbybot immediately.
*/
function sendRegisterGameStanzaImmediate()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
if (g_GameStanzaTimer !== undefined)
{
clearTimeout(g_GameStanzaTimer);
g_GameStanzaTimer = undefined;
}
let clients = formatClientsForStanza();
let stanza = {
"name": g_ServerName,
"port": g_ServerPort,
"hostUsername": Engine.LobbyGetNick(),
"mapName": g_GameAttributes.map,
"niceMapName": getMapDisplayName(g_GameAttributes.map),
"mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default",
"mapType": g_GameAttributes.mapType,
- "victoryCondition": g_GameAttributes.settings.GameType,
+ "victoryConditions": g_GameAttributes.settings.VictoryConditions.join(","),
"nbp": clients.connectedPlayers,
"maxnbp": g_GameAttributes.settings.PlayerData.length,
"players": clients.list,
"stunIP": g_StunEndpoint ? g_StunEndpoint.ip : "",
"stunPort": g_StunEndpoint ? g_StunEndpoint.port : "",
"mods": JSON.stringify(Engine.GetEngineInfo().mods),
};
// Only send the stanza if the relevant settings actually changed
if (g_LastGameStanza && Object.keys(stanza).every(prop => g_LastGameStanza[prop] == stanza[prop]))
return;
g_LastGameStanza = stanza;
Engine.SendRegisterGame(stanza);
}
/**
* Send the relevant gamesettings to the lobbybot in a deferred manner.
*/
function sendRegisterGameStanza()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
if (g_GameStanzaTimer !== undefined)
clearTimeout(g_GameStanzaTimer);
g_GameStanzaTimer = setTimeout(sendRegisterGameStanzaImmediate, g_GameStanzaTimeout * 1000);
}
/**
* Figures out all strings that can be autocompleted and sorts
* them by priority (so that playernames are always autocompleted first).
*/
function updateAutocompleteEntries()
{
let autocomplete = { "0": [] };
for (let control of [g_Dropdowns, g_Checkboxes])
for (let name in control)
autocomplete[0] = autocomplete[0].concat(control[name].title());
for (let dropdown of [g_Dropdowns, g_PlayerDropdowns])
for (let name in dropdown)
{
let priority = dropdown[name].autocomplete;
if (priority === undefined)
continue;
autocomplete[priority] = (autocomplete[priority] || []).concat(dropdown[name].labels());
}
g_Autocomplete = Object.keys(autocomplete).sort().reverse().reduce((all, priority) => all.concat(autocomplete[priority]), []);
}
function storeCivInfoPage(data)
{
g_CivInfo.code = data.civ;
g_CivInfo.page = data.page;
}
Index: ps/trunk/binaries/data/mods/public/gui/loadgame/load.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/loadgame/load.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/gui/loadgame/load.js (revision 21474)
@@ -1,203 +1,203 @@
var g_SavedGamesMetadata = [];
/**
* Needed for formatPlayerInfo to show the player civs in the details.
*/
const g_CivData = loadCivData(false, false);
function init()
{
let savedGames = Engine.GetSavedGames();
// Get current game version and loaded mods
let engineInfo = Engine.GetEngineInfo();
if (Engine.GetGUIObjectByName("compatibilityFilter").checked)
savedGames = savedGames.filter(game => isCompatibleSavegame(game.metadata, engineInfo));
let gameSelection = Engine.GetGUIObjectByName("gameSelection");
gameSelection.enabled = !!savedGames.length;
Engine.GetGUIObjectByName("gameSelectionFeedback").hidden = !!savedGames.length;
let selectedGameId = gameSelection.list_data[gameSelection.selected];
// Save metadata for the detailed view
g_SavedGamesMetadata = savedGames.map(game => {
game.metadata.id = game.id;
return game.metadata;
});
let sortKey = gameSelection.selected_column;
let sortOrder = gameSelection.selected_column_order;
g_SavedGamesMetadata = g_SavedGamesMetadata.sort((a, b) => {
let cmpA, cmpB;
switch (sortKey)
{
case 'date':
cmpA = +a.time;
cmpB = +b.time;
break;
case 'mapName':
cmpA = translate(a.initAttributes.settings.Name);
cmpB = translate(b.initAttributes.settings.Name);
break;
case 'mapType':
cmpA = translateMapType(a.initAttributes.mapType);
cmpB = translateMapType(b.initAttributes.mapType);
break;
case 'description':
cmpA = a.description;
cmpB = b.description;
break;
}
if (cmpA < cmpB)
return -sortOrder;
else if (cmpA > cmpB)
return +sortOrder;
return 0;
});
let list = g_SavedGamesMetadata.map(metadata => {
let isCompatible = isCompatibleSavegame(metadata, engineInfo);
return {
"date": generateSavegameDateString(metadata, engineInfo),
"mapName": compatibilityColor(translate(metadata.initAttributes.settings.Name), isCompatible),
"mapType": compatibilityColor(translateMapType(metadata.initAttributes.mapType), isCompatible),
"description": compatibilityColor(metadata.description, isCompatible)
};
});
if (list.length)
list = prepareForDropdown(list);
gameSelection.list_date = list.date || [];
gameSelection.list_mapName = list.mapName || [];
gameSelection.list_mapType = list.mapType || [];
gameSelection.list_description = list.description || [];
// Change these last, otherwise crash
// list strings used in the delete dialog
gameSelection.list = g_SavedGamesMetadata.map(metadata => generateSavegameLabel(metadata, engineInfo));
gameSelection.list_data = g_SavedGamesMetadata.map(metadata => metadata.id);
let selectedGameIndex = g_SavedGamesMetadata.findIndex(metadata => metadata.id == selectedGameId);
if (selectedGameIndex != -1)
gameSelection.selected = selectedGameIndex;
else if (gameSelection.selected >= g_SavedGamesMetadata.length) // happens when deleting the last saved game
gameSelection.selected = g_SavedGamesMetadata.length - 1;
else if (gameSelection.selected == -1 && g_SavedGamesMetadata.length)
gameSelection.selected = 0;
selectionChanged();
Engine.GetGUIObjectByName("deleteGameButton").tooltip = deleteTooltip();
}
function selectionChanged()
{
let metadata = g_SavedGamesMetadata[Engine.GetGUIObjectByName("gameSelection").selected];
Engine.GetGUIObjectByName("invalidGame").hidden = !!metadata;
Engine.GetGUIObjectByName("validGame").hidden = !metadata;
Engine.GetGUIObjectByName("loadGameButton").enabled = !!metadata;
Engine.GetGUIObjectByName("deleteGameButton").enabled = !!metadata;
if (!metadata)
return;
Engine.GetGUIObjectByName("savedMapName").caption = translate(metadata.initAttributes.settings.Name);
let mapData = getMapDescriptionAndPreview(metadata.initAttributes.mapType, metadata.initAttributes.map);
setMapPreviewImage("savedInfoPreview", mapData.preview);
Engine.GetGUIObjectByName("savedPlayers").caption = metadata.initAttributes.settings.PlayerData.length - 1;
Engine.GetGUIObjectByName("savedPlayedTime").caption = timeToString(metadata.gui.timeElapsed ? metadata.gui.timeElapsed : 0);
Engine.GetGUIObjectByName("savedMapType").caption = translateMapType(metadata.initAttributes.mapType);
Engine.GetGUIObjectByName("savedMapSize").caption = translateMapSize(metadata.initAttributes.settings.Size);
- Engine.GetGUIObjectByName("savedVictory").caption = translateVictoryCondition(metadata.initAttributes.settings.GameType);
+ Engine.GetGUIObjectByName("savedVictory").caption = metadata.initAttributes.settings.VictoryConditions.map(victoryConditionName => translateVictoryCondition(victoryConditionName)).join(translate(", "));
let caption = sprintf(translate("Mods: %(mods)s"), { "mods": modsToString(metadata.mods) });
if (!hasSameMods(metadata.mods, Engine.GetEngineInfo().mods))
caption = coloredText(caption, "orange");
Engine.GetGUIObjectByName("savedMods").caption = caption;
Engine.GetGUIObjectByName("savedPlayersNames").caption = formatPlayerInfo(
metadata.initAttributes.settings.PlayerData,
metadata.gui.states
);
}
function loadGame()
{
let gameSelection = Engine.GetGUIObjectByName("gameSelection");
let gameId = gameSelection.list_data[gameSelection.selected];
let metadata = g_SavedGamesMetadata[gameSelection.selected];
// Check compatibility before really loading it
let engineInfo = Engine.GetEngineInfo();
let sameMods = hasSameMods(metadata.mods, engineInfo.mods);
let sameEngineVersion = hasSameEngineVersion(metadata, engineInfo);
if (sameEngineVersion && sameMods)
{
reallyLoadGame(gameId);
return;
}
// Version not compatible ... ask for confirmation
let message = "";
if (!sameEngineVersion)
if (metadata.engine_version)
message += sprintf(translate("This savegame needs 0 A.D. version %(requiredVersion)s, while you are running version %(currentVersion)s."), {
"requiredVersion": metadata.engine_version,
"currentVersion": engineInfo.engine_version
}) + "\n";
else
message += translate("This savegame needs an older version of 0 A.D.") + "\n";
if (!sameMods)
{
if (!metadata.mods)
metadata.mods = [];
message += translate("This savegame needs a different sequence of mods:") + "\n" +
comparedModsString(metadata.mods, engineInfo.mods) + "\n";
}
message += translate("Do you still want to proceed?");
messageBox(
500, 250,
message,
translate("Warning"),
[translate("No"), translate("Yes")],
[init, function(){ reallyLoadGame(gameId); }]
);
}
function reallyLoadGame(gameId)
{
let metadata = Engine.StartSavedGame(gameId);
if (!metadata)
{
// Probably the file wasn't found
// Show error and refresh saved game list
error("Could not load saved game: " + gameId);
init();
return;
}
let pData = metadata.initAttributes.settings.PlayerData[metadata.playerID];
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": metadata.initAttributes,
"isNetworked": false,
"playerAssignments": {
"local": {
"name": pData ? pData.Name : singleplayerName(),
"player": metadata.playerID
}
},
"savedGUIData": metadata.gui
});
}
Index: ps/trunk/binaries/data/mods/public/gui/loadgame/load.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/loadgame/load.xml (revision 21473)
+++ ps/trunk/binaries/data/mods/public/gui/loadgame/load.xml (revision 21474)
@@ -1,117 +1,117 @@
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_filters.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_filters.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_filters.js (revision 21474)
@@ -1,299 +1,300 @@
/**
* Allow to filter replays by duration in 15min / 30min intervals.
*/
var g_DurationFilterIntervals = [
{ "min": -1, "max": -1 },
{ "min": -1, "max": 15 },
{ "min": 15, "max": 30 },
{ "min": 30, "max": 45 },
{ "min": 45, "max": 60 },
{ "min": 60, "max": 90 },
{ "min": 90, "max": 120 },
{ "min": 120, "max": -1 }
];
/**
* Allow to filter by population capacity.
*/
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
/**
* Reloads the selectable values in the filters. The filters depend on g_Settings and g_Replays
* (including its derivatives g_MapSizes, g_MapNames).
*/
function initFilters(filters)
{
Engine.GetGUIObjectByName("compatibilityFilter").checked = !filters || filters.compatibility === undefined || filters.compatibility;
if (filters && filters.playernames)
Engine.GetGUIObjectByName("playersFilter").caption = filters.playernames;
initDateFilter(filters);
initMapSizeFilter(filters);
initMapNameFilter(filters);
initPopCapFilter(filters);
initDurationFilter(filters);
initSingleplayerFilter(filters);
initVictoryConditionFilter(filters);
initRatedGamesFilter(filters);
}
/**
* Allow to filter by month. Uses g_Replays.
*/
function initDateFilter(filters)
{
var months = g_Replays.map(replay => getReplayMonth(replay));
months = months.filter((month, index) => months.indexOf(month) == index).sort();
var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
dateTimeFilter.list = [translateWithContext("datetime", "Any")].concat(months);
dateTimeFilter.list_data = [""].concat(months);
if (filters && filters.date)
dateTimeFilter.selected = dateTimeFilter.list_data.indexOf(filters.date);
if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= dateTimeFilter.list.length)
dateTimeFilter.selected = 0;
}
/**
* Allow to filter by mapsize. Uses g_MapSizes.
*/
function initMapSizeFilter(filters)
{
var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
mapSizeFilter.list = [translateWithContext("map size", "Any")].concat(g_MapSizes.Name);
mapSizeFilter.list_data = [-1].concat(g_MapSizes.Tiles);
if (filters && filters.mapSize)
mapSizeFilter.selected = mapSizeFilter.list_data.indexOf(filters.mapSize);
if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= mapSizeFilter.list.length)
mapSizeFilter.selected = 0;
}
/**
* Allow to filter by mapname. Uses g_MapNames.
*/
function initMapNameFilter(filters)
{
var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
mapNameFilter.list = [translateWithContext("map name", "Any")].concat(g_MapNames.map(name => translate(name)));
mapNameFilter.list_data = [""].concat(g_MapNames);
if (filters && filters.mapName)
mapNameFilter.selected = mapNameFilter.list_data.indexOf(filters.mapName);
if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNameFilter.list.length)
mapNameFilter.selected = 0;
}
/**
* Allow to filter by population capacity.
*/
function initPopCapFilter(filters)
{
var populationFilter = Engine.GetGUIObjectByName("populationFilter");
populationFilter.list = [translateWithContext("population capacity", "Any")].concat(g_PopulationCapacities.Title);
populationFilter.list_data = [""].concat(g_PopulationCapacities.Population);
if (filters && filters.popCap)
populationFilter.selected = populationFilter.list_data.indexOf(filters.popCap);
if (populationFilter.selected == -1 || populationFilter.selected >= populationFilter.list.length)
populationFilter.selected = 0;
}
/**
* Allow to filter by game duration. Uses g_DurationFilterIntervals.
*/
function initDurationFilter(filters)
{
var durationFilter = Engine.GetGUIObjectByName("durationFilter");
durationFilter.list = g_DurationFilterIntervals.map((interval, index) => {
if (index == 0)
return translateWithContext("duration", "Any");
if (index == 1)
// Translation: Shorter duration than max minutes.
return sprintf(translatePluralWithContext("duration filter", "< %(max)s min", "< %(max)s min", interval.max), interval);
if (index == g_DurationFilterIntervals.length - 1)
// Translation: Longer duration than min minutes.
return sprintf(translatePluralWithContext("duration filter", "> %(min)s min", "> %(min)s min", interval.min), interval);
// Translation: Duration between min and max minutes.
return sprintf(translateWithContext("duration filter", "%(min)s - %(max)s min"), interval);
});
durationFilter.list_data = g_DurationFilterIntervals.map((interval, index) => index);
if (filters && filters.duration)
durationFilter.selected = durationFilter.list_data.indexOf(filters.duration);
if (durationFilter.selected == -1 || durationFilter.selected >= g_DurationFilterIntervals.length)
durationFilter.selected = 0;
}
function initSingleplayerFilter(filters)
{
let singleplayerFilter = Engine.GetGUIObjectByName("singleplayerFilter");
singleplayerFilter.list = [translate("Single- and multiplayer"), translate("Single Player"), translate("Multiplayer")];
singleplayerFilter.list_data = ["", "Singleplayer", "Multiplayer"];
if (filters && filters.singleplayer)
singleplayerFilter.selected = singleplayerFilter.list_data.indexOf(filters.singleplayer);
if (singleplayerFilter.selected < 0 || singleplayerFilter.selected >= singleplayerFilter.list.length)
singleplayerFilter.selected = 0;
}
function initVictoryConditionFilter(filters)
{
let victoryConditionFilter = Engine.GetGUIObjectByName("victoryConditionFilter");
- victoryConditionFilter.list = [translateWithContext("victory condition", "Any gametype")].concat(g_VictoryConditions.map(vc => translateVictoryCondition(vc)));
- victoryConditionFilter.list_data = [""].concat(g_VictoryConditions);
+ victoryConditionFilter.list = [translate("Any Victory Condition")].concat(g_VictoryConditions.map(victoryCondition => translateVictoryCondition(victoryCondition.Name)));
+ victoryConditionFilter.list_data = [""].concat(g_VictoryConditions.map(victoryCondition => victoryCondition.Name));
if (filters && filters.victoryCondition)
victoryConditionFilter.selected = victoryConditionFilter.list_data.indexOf(filters.victoryCondition);
if (victoryConditionFilter.selected < 0 || victoryConditionFilter.selected >= victoryConditionFilter.list.length)
victoryConditionFilter.selected = 0;
}
function initRatedGamesFilter(filters)
{
let ratedGamesFilter = Engine.GetGUIObjectByName("ratedGamesFilter");
ratedGamesFilter.list = [translate("Rated and unrated games"), translate("Rated games"), translate("Unrated games")];
ratedGamesFilter.list_data = ["", "rated", "not rated"];
if (filters && filters.ratedGames)
ratedGamesFilter.selected = ratedGamesFilter.list_data.indexOf(filters.ratedGames);
if (ratedGamesFilter.selected < 0 || ratedGamesFilter.selected >= ratedGamesFilter.list.length)
ratedGamesFilter.selected = 0;
}
/**
* Initializes g_ReplaysFiltered with replays that are not filtered out and sort it.
*/
function filterReplays()
{
let sortKey = Engine.GetGUIObjectByName("replaySelection").selected_column;
let sortOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order;
g_ReplaysFiltered = g_Replays.filter(replay => filterReplay(replay)).sort((a, b) => {
let cmpA, cmpB;
switch (sortKey)
{
case 'months':
cmpA = +a.attribs.timestamp;
cmpB = +b.attribs.timestamp;
break;
case 'duration':
cmpA = +a.duration;
cmpB = +b.duration;
break;
case 'players':
cmpA = +a.attribs.settings.PlayerData.length;
cmpB = +b.attribs.settings.PlayerData.length;
break;
case 'mapName':
cmpA = getReplayMapName(a);
cmpB = getReplayMapName(b);
break;
case 'mapSize':
cmpA = +a.attribs.settings.Size;
cmpB = +b.attribs.settings.Size;
break;
case 'popCapacity':
cmpA = +a.attribs.settings.PopulationCap;
cmpB = +b.attribs.settings.PopulationCap;
break;
}
if (cmpA < cmpB)
return -sortOrder;
else if (cmpA > cmpB)
return +sortOrder;
return 0;
});
}
/**
* Decides whether the replay should be listed.
*
* @returns {bool} - true if replay should be visible
*/
function filterReplay(replay)
{
// Check for compatibility first (most likely to filter)
let compatibilityFilter = Engine.GetGUIObjectByName("compatibilityFilter");
if (compatibilityFilter.checked && !isReplayCompatible(replay))
return false;
// Filter by singleplayer / multiplayer
let singleplayerFilter = Engine.GetGUIObjectByName("singleplayerFilter");
let selectedSingleplayerFilter = singleplayerFilter.list_data[singleplayerFilter.selected] || "";
if (selectedSingleplayerFilter == "Singleplayer" && replay.isMultiplayer ||
selectedSingleplayerFilter == "Multiplayer" && !replay.isMultiplayer)
return false;
// Filter by victory condition
let victoryConditionFilter = Engine.GetGUIObjectByName("victoryConditionFilter");
- if (victoryConditionFilter.selected > 0 && replay.attribs.settings.GameType != victoryConditionFilter.list_data[victoryConditionFilter.selected])
+ if (victoryConditionFilter.selected > 0 &&
+ replay.attribs.settings.VictoryConditions.indexOf(victoryConditionFilter.list_data[victoryConditionFilter.selected]) == -1)
return false;
// Filter by rating
let ratedGamesFilter = Engine.GetGUIObjectByName("ratedGamesFilter");
let selectedRatedGamesFilter = ratedGamesFilter.list_data[ratedGamesFilter.selected] || "";
if (selectedRatedGamesFilter == "rated" && !replay.isRated ||
selectedRatedGamesFilter == "not rated" && replay.isRated)
return false;
// Filter date/time (select a month)
let dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
if (dateTimeFilter.selected > 0 && getReplayMonth(replay) != dateTimeFilter.list_data[dateTimeFilter.selected])
return false;
// Filter by playernames
let playersFilter = Engine.GetGUIObjectByName("playersFilter");
let keywords = playersFilter.caption.toLowerCase().split(" ");
if (keywords.length)
{
// We just check if all typed words are somewhere in the playerlist of that replay
let playerList = replay.attribs.settings.PlayerData.map(player => player ? player.Name : "").join(" ").toLowerCase();
if (!keywords.every(keyword => playerList.indexOf(keyword) != -1))
return false;
}
// Filter by map name
let mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
if (mapNameFilter.selected > 0 && getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected])
return false;
// Filter by map size
let mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
if (mapSizeFilter.selected > 0 && replay.attribs.settings.Size != mapSizeFilter.list_data[mapSizeFilter.selected])
return false;
// Filter by population capacity
let populationFilter = Engine.GetGUIObjectByName("populationFilter");
if (populationFilter.selected > 0 && replay.attribs.settings.PopulationCap != populationFilter.list_data[populationFilter.selected])
return false;
// Filter by game duration
let durationFilter = Engine.GetGUIObjectByName("durationFilter");
if (durationFilter.selected > 0)
{
let interval = g_DurationFilterIntervals[durationFilter.selected];
if ((interval.min > -1 && replay.duration < interval.min * 60) ||
(interval.max > -1 && replay.duration > interval.max * 60))
return false;
}
return true;
}
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 21474)
@@ -1,364 +1,357 @@
/**
* Used for checking replay compatibility.
*/
const g_EngineInfo = Engine.GetEngineInfo();
/**
* Needed for formatPlayerInfo to show the player civs in the details.
*/
const g_CivData = loadCivData(false, false);
/**
* Used for creating the mapsize filter.
*/
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
/**
* All replays found in the directory.
*/
var g_Replays = [];
/**
* List of replays after applying the display filter.
*/
var g_ReplaysFiltered = [];
/**
* Array of unique usernames of all replays. Used for autocompleting usernames.
*/
var g_Playernames = [];
/**
* Sorted list of unique maptitles. Used by mapfilter.
*/
var g_MapNames = [];
/**
* Sorted list of the victory conditions occuring in the replays
*/
-var g_VictoryConditions = [];
+var g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
/**
* Directory name of the currently selected replay. Used to restore the selection after changing filters.
*/
var g_SelectedReplayDirectory = "";
/**
* Skip duplicate expensive GUI updates before init is complete.
*/
var g_ReplaysLoaded = false;
/**
* Remember last viewed summary panel and charts.
*/
var g_SummarySelectedData;
/**
* Initializes globals, loads replays and displays the list.
*/
function init(data)
{
if (!g_Settings)
{
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
loadReplays(data && data.replaySelectionData, false);
if (!g_Replays)
{
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
initHotkeyTooltips();
displayReplayList();
if (data && data.summarySelectedData)
g_SummarySelectedData = data.summarySelectedData;
}
/**
* Store the list of replays loaded in C++ in g_Replays.
* Check timestamp and compatibility and extract g_Playernames, g_MapNames, g_VictoryConditions.
* Restore selected filters and item.
* @param replaySelectionData - Currently selected filters and item to be restored after the loading.
* @param compareFiles - If true, compares files briefly (which might be slow with optical harddrives),
* otherwise blindly trusts the replay cache.
*/
function loadReplays(replaySelectionData, compareFiles)
{
g_Replays = Engine.GetReplays(compareFiles);
if (!g_Replays)
return;
g_Playernames = [];
for (let replay of g_Replays)
{
let nonAIPlayers = 0;
// Check replay for compatibility
replay.isCompatible = isReplayCompatible(replay);
sanitizeGameAttributes(replay.attribs);
// Extract map names
if (g_MapNames.indexOf(replay.attribs.settings.Name) == -1 && replay.attribs.settings.Name != "")
g_MapNames.push(replay.attribs.settings.Name);
- // Extract victory conditions
- if (replay.attribs.settings.GameType && g_VictoryConditions.indexOf(replay.attribs.settings.GameType) == -1)
- g_VictoryConditions.push(replay.attribs.settings.GameType);
-
// Extract playernames
for (let playerData of replay.attribs.settings.PlayerData)
{
if (!playerData || playerData.AI)
continue;
// Remove rating from nick
let playername = playerData.Name;
let ratingStart = playername.indexOf(" (");
if (ratingStart != -1)
playername = playername.substr(0, ratingStart);
if (g_Playernames.indexOf(playername) == -1)
g_Playernames.push(playername);
++nonAIPlayers;
}
replay.isMultiplayer = nonAIPlayers > 1;
replay.isRated = nonAIPlayers == 2 &&
replay.attribs.settings.PlayerData.length == 2 &&
replay.attribs.settings.RatingEnabled;
}
g_MapNames.sort();
- g_VictoryConditions.sort();
// Reload filters (since they depend on g_Replays and its derivatives)
initFilters(replaySelectionData && replaySelectionData.filters);
// Restore user selection
if (replaySelectionData)
{
if (replaySelectionData.directory)
g_SelectedReplayDirectory = replaySelectionData.directory;
let replaySelection = Engine.GetGUIObjectByName("replaySelection");
if (replaySelectionData.column)
replaySelection.selected_column = replaySelectionData.column;
if (replaySelectionData.columnOrder)
replaySelection.selected_column_order = replaySelectionData.columnOrder;
}
g_ReplaysLoaded = true;
}
/**
* We may encounter malformed replays.
*/
function sanitizeGameAttributes(attribs)
{
if (!attribs.settings)
attribs.settings = {};
if (!attribs.settings.Size)
attribs.settings.Size = -1;
if (!attribs.settings.Name)
attribs.settings.Name = "";
if (!attribs.settings.PlayerData)
attribs.settings.PlayerData = [];
if (!attribs.settings.PopulationCap)
attribs.settings.PopulationCap = 300;
if (!attribs.settings.mapType)
attribs.settings.mapType = "skirmish";
- if (!attribs.settings.GameType)
- attribs.settings.GameType = "conquest";
-
// Remove gaia
if (attribs.settings.PlayerData.length && attribs.settings.PlayerData[0] == null)
attribs.settings.PlayerData.shift();
attribs.settings.PlayerData.forEach((pData, index) => {
if (!pData.Name)
pData.Name = "";
});
}
function initHotkeyTooltips()
{
Engine.GetGUIObjectByName("playersFilter").tooltip =
translate("Filter replays by typing one or more, partial or complete playernames.") +
" " + colorizeAutocompleteHotkey();
Engine.GetGUIObjectByName("deleteReplayButton").tooltip = deleteTooltip();
}
/**
* Filter g_Replays, fill the GUI list with that data and show the description of the current replay.
*/
function displayReplayList()
{
if (!g_ReplaysLoaded)
return;
// Remember previously selected replay
var replaySelection = Engine.GetGUIObjectByName("replaySelection");
if (replaySelection.selected != -1)
g_SelectedReplayDirectory = g_ReplaysFiltered[replaySelection.selected].directory;
filterReplays();
var list = g_ReplaysFiltered.map(replay => {
let works = replay.isCompatible;
return {
"directories": replay.directory,
"months": compatibilityColor(getReplayDateTime(replay), works),
"popCaps": compatibilityColor(translatePopulationCapacity(replay.attribs.settings.PopulationCap), works),
"mapNames": compatibilityColor(getReplayMapName(replay), works),
"mapSizes": compatibilityColor(translateMapSize(replay.attribs.settings.Size), works),
"durations": compatibilityColor(getReplayDuration(replay), works),
"playerNames": compatibilityColor(getReplayPlayernames(replay), works)
};
});
if (list.length)
list = prepareForDropdown(list);
// Push to GUI
replaySelection.selected = -1;
replaySelection.list_months = list.months || [];
replaySelection.list_players = list.playerNames || [];
replaySelection.list_mapName = list.mapNames || [];
replaySelection.list_mapSize = list.mapSizes || [];
replaySelection.list_popCapacity = list.popCaps || [];
replaySelection.list_duration = list.durations || [];
// Change these last, otherwise crash
replaySelection.list = list.directories || [];
replaySelection.list_data = list.directories || [];
replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_SelectedReplayDirectory);
displayReplayDetails();
}
/**
* Shows preview image, description and player text in the right panel.
*/
function displayReplayDetails()
{
let selected = Engine.GetGUIObjectByName("replaySelection").selected;
let replaySelected = selected > -1;
Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected;
Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected;
Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected;
Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected;
Engine.GetGUIObjectByName("replayFilename").hidden = !replaySelected;
Engine.GetGUIObjectByName("summaryButton").hidden = true;
if (!replaySelected)
return;
let replay = g_ReplaysFiltered[selected];
Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.Name);
Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(replay.attribs.settings.Size);
Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(replay.attribs.settings.mapType);
- Engine.GetGUIObjectByName("sgVictory").caption = translateVictoryCondition(replay.attribs.settings.GameType);
+ Engine.GetGUIObjectByName("sgVictory").caption = replay.attribs.settings.VictoryConditions.map(victoryConditionName =>
+ translateVictoryCondition(victoryConditionName)).join(translate(", "));
Engine.GetGUIObjectByName("sgNbPlayers").caption = sprintf(translate("Players: %(numberOfPlayers)s"),
{ "numberOfPlayers": replay.attribs.settings.PlayerData.length });
Engine.GetGUIObjectByName("replayFilename").caption = Engine.GetReplayDirectoryName(replay.directory);
let metadata = Engine.GetReplayMetadata(replay.directory);
Engine.GetGUIObjectByName("sgPlayersNames").caption =
formatPlayerInfo(
replay.attribs.settings.PlayerData,
Engine.GetGUIObjectByName("showSpoiler").checked &&
metadata &&
metadata.playerStates &&
metadata.playerStates.map(pState => pState.state)
);
let mapData = getMapDescriptionAndPreview(replay.attribs.settings.mapType, replay.attribs.map);
Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description;
Engine.GetGUIObjectByName("summaryButton").hidden = !Engine.HasReplayMetadata(replay.directory);
setMapPreviewImage("sgMapPreview", mapData.preview);
}
/**
* Returns a human-readable version of the replay date.
*/
function getReplayDateTime(replay)
{
return Engine.FormatMillisecondsIntoDateStringLocal(replay.attribs.timestamp * 1000, translate("yyyy-MM-dd HH:mm"));
}
/**
* Returns a human-readable list of the playernames of that replay.
*
* @returns {string}
*/
function getReplayPlayernames(replay)
{
return replay.attribs.settings.PlayerData.map(pData => pData.Name).join(", ");
}
/**
* Returns the name of the map of the given replay.
*
* @returns {string}
*/
function getReplayMapName(replay)
{
return translate(replay.attribs.settings.Name);
}
/**
* Returns the month of the given replay in the format "yyyy-MM".
*
* @returns {string}
*/
function getReplayMonth(replay)
{
return Engine.FormatMillisecondsIntoDateStringLocal(replay.attribs.timestamp * 1000, translate("yyyy-MM"));
}
/**
* Returns a human-readable version of the time when the replay started.
*
* @returns {string}
*/
function getReplayDuration(replay)
{
return timeToString(replay.duration * 1000);
}
/**
* True if we can start the given replay with the currently loaded mods.
*/
function isReplayCompatible(replay)
{
return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs.mods, g_EngineInfo.mods);
}
/**
* True if we can start the given replay with the currently loaded mods.
*/
function replayHasSameEngineVersion(replay)
{
return replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version;
}
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml (revision 21473)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml (revision 21474)
@@ -1,256 +1,256 @@
Replay Games
displayReplayList();
displayReplayList();
autoCompleteNick(this, g_Playernames);
displayReplayList();
displayReplayList();
displayReplayList();
displayReplayList();
displayReplayDetails();
displayReplayList();
startReplay();
Date / Time
Players
Map Name
Size
Population
Duration
displayReplayList();
Filter compatible replays
displayReplayList();
displayReplayList();
displayReplayList();
Map Type:
Map Size:
-
+
Victory:
-
-
+
+
-
+
-
+
displayReplayDetails();
Spoiler
Main Menu
Engine.SwitchGuiPage("page_pregame.xml");
Delete
deleteReplayButtonPressed();
Reload Cache
Rebuild the replay cache from scratch. Potentially slow!
reloadCache();
Summary
showReplaySummary();
Start Replay
startReplay();
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 21474)
@@ -1,1771 +1,1772 @@
const g_IsReplay = Engine.IsVisualReplay();
const g_CivData = loadCivData(false, true);
const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
-const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions);
const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
+const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
+
var g_GameSpeeds;
/**
* Whether to display diplomacy colors (where players see self/ally/neutral/enemy each in different colors and
* observers see each team in a different color) or regular player colors.
*/
var g_DiplomacyColorsToggle = false;
/**
* The array of displayed player colors (either the diplomacy color or regular color for each player).
*/
var g_DisplayedPlayerColors;
/**
* The self/ally/neutral/enemy color codes.
*/
var g_DiplomacyColorPalette;
/**
* Colors to flash when pop limit reached.
*/
var g_DefaultPopulationColor = "white";
var g_PopulationAlertColor = "orange";
/**
* Seen in the tooltip of the top panel.
*/
var g_ResourceTitleFont = "sans-bold-16";
/**
* A random file will be played. TODO: more variety
*/
var g_Ambient = ["audio/ambient/dayscape/day_temperate_gen_03.ogg"];
/**
* Map, player and match settings set in gamesetup.
*/
const g_GameAttributes = deepfreeze(Engine.GetInitAttributes());
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
*/
var g_IsController;
/**
* True if this is a multiplayer game.
*/
var g_IsNetworked = false;
/**
* Whether we have finished the synchronization and
* can start showing simulation related message boxes.
*/
var g_IsNetworkedActive = false;
/**
* True if the connection to the server has been lost.
*/
var g_Disconnected = false;
/**
* True if the current user has observer capabilities.
*/
var g_IsObserver = false;
/**
* True if the current user has rejoined (or joined the game after it started).
*/
var g_HasRejoined = false;
/**
* Shows a message box asking the user to leave if "won" or "defeated".
*/
var g_ConfirmExit = false;
/**
* True if the current player has paused the game explicitly.
*/
var g_Paused = false;
/**
* The list of GUIDs of players who have currently paused the game, if the game is networked.
*/
var g_PausingClients = [];
/**
* The playerID selected in the change perspective tool.
*/
var g_ViewedPlayer = Engine.GetPlayerID();
/**
* True if the camera should focus on attacks and player commands
* and select the affected units.
*/
var g_FollowPlayer = false;
/**
* Cache the basic player data (name, civ, color).
*/
var g_Players = [];
/**
* Last time when onTick was called().
* Used for animating the main menu.
*/
var g_LastTickTime = Date.now();
/**
* Recalculate which units have their status bars shown with this frequency in milliseconds.
*/
var g_StatusBarUpdate = 200;
/**
* For restoring selection, order and filters when returning to the replay menu
*/
var g_ReplaySelectionData;
var g_PlayerAssignments = {
"local": {
"name": singleplayerName(),
"player": 1
}
};
/**
* Cache dev-mode settings that are frequently or widely used.
*/
var g_DevSettings = {
"changePerspective": false,
"controlAll": false
};
/**
* Whether the entire UI should be hidden (useful for promotional screenshots).
* Can be toggled with a hotkey.
*/
var g_ShowGUI = true;
/**
* Whether status bars should be shown for all of the player's units.
*/
var g_ShowAllStatusBars = false;
/**
* Blink the population counter if the player can't train more units.
*/
var g_IsTrainingBlocked = false;
/**
* Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update).
*/
var g_SimState;
var g_EntityStates = {};
var g_TemplateData = {};
var g_TechnologyData = {};
var g_ResourceData = new Resources();
/**
* Top coordinate of the research list.
* Changes depending on the number of displayed counters.
*/
var g_ResearchListTop = 4;
/**
* List of additional entities to highlight.
*/
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];
/**
* Display data of the current players entities shown in the top panel.
*/
var g_PanelEntities = [];
/**
* Order in which the panel entities are shown.
*/
var g_PanelEntityOrder = ["Hero", "Relic"];
/**
* Unit classes to be checked for the idle-worker-hotkey.
*/
var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "CitizenSoldier"];
/**
* Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey.
*/
var g_MilitaryTypes = ["Melee", "Ranged"];
function GetSimState()
{
if (!g_SimState)
g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState"));
return g_SimState;
}
function GetMultipleEntityStates(ents)
{
if (!ents.length)
return null;
let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents);
for (let item of entityStates)
g_EntityStates[item.entId] = item.state && deepfreeze(item.state);
return entityStates;
}
function GetEntityState(entId)
{
if (!g_EntityStates[entId])
{
let entityState = Engine.GuiInterfaceCall("GetEntityState", entId);
g_EntityStates[entId] = entityState && deepfreeze(entityState);
}
return g_EntityStates[entId];
}
function GetTemplateData(templateName)
{
if (!(templateName in g_TemplateData))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
translateObjectKeys(template, ["specific", "generic", "tooltip"]);
g_TemplateData[templateName] = deepfreeze(template);
}
return g_TemplateData[templateName];
}
function GetTechnologyData(technologyName, civ)
{
if (!g_TechnologyData[civ])
g_TechnologyData[civ] = {};
if (!(technologyName in g_TechnologyData[civ]))
{
let template = GetTechnologyDataHelper(TechnologyTemplates.Get(technologyName), civ, g_ResourceData);
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
g_TechnologyData[civ][technologyName] = deepfreeze(template);
}
return g_TechnologyData[civ][technologyName];
}
function init(initData, hotloadData)
{
if (!g_Settings)
{
Engine.EndGame();
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
if (initData)
{
g_IsNetworked = initData.isNetworked;
g_IsController = initData.isController;
g_PlayerAssignments = initData.playerAssignments;
g_ReplaySelectionData = initData.replaySelectionData;
g_HasRejoined = initData.isRejoining;
if (initData.savedGUIData)
restoreSavedGameData(initData.savedGUIData);
Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
}
else if (g_IsReplay)// Needed for autostart loading option
g_PlayerAssignments.local.player = -1;
LoadModificationTemplates();
updatePlayerData();
g_BarterSell = g_ResourceData.GetCodes()[0];
initializeMusic(); // before changing the perspective
initSessionMenuButtons();
for (let slot in Engine.GetGUIObjectByName("panelEntityPanel").children)
initPanelEntities(slot);
g_DiplomacyColorPalette = Engine.ReadJSONFile(g_SettingsDirectory + "diplomacy_colors.json");
g_DisplayedPlayerColors = g_Players.map(player => player.color);
updateViewedPlayerDropdown();
// Select "observer" in the view player dropdown when rejoining as a defeated player
let player = g_Players[Engine.GetPlayerID()];
Engine.GetGUIObjectByName("viewPlayer").selected = player && player.state == "defeated" ? 0 : Engine.GetPlayerID() + 1;
// If in Atlas editor, disable the exit button
if (Engine.IsAtlasRunning())
Engine.GetGUIObjectByName("menuExitButton").enabled = false;
if (hotloadData)
g_Selection.selected = hotloadData.selection;
Engine.SetBoundingBoxDebugOverlay(false);
initChatWindow();
sendLobbyPlayerlistUpdate();
onSimulationUpdate();
setTimeout(displayGamestateNotifications, 1000);
// Report the performance after 5 seconds (when we're still near
// the initial camera view) and a minute (when the profiler will
// have settled down if framerates as very low), to give some
// extremely rough indications of performance
//
// DISABLED: this information isn't currently useful for anything much,
// and it generates a massive amount of data to transmit and store
// setTimeout(function() { reportPerformance(5); }, 5000);
// setTimeout(function() { reportPerformance(60); }, 60000);
}
function updatePlayerData()
{
let simState = GetSimState();
if (!simState)
return;
let playerData = [];
for (let i = 0; i < simState.players.length; ++i)
{
let playerState = simState.players[i];
playerData.push({
"name": playerState.name,
"civ": playerState.civ,
"color": {
"r": playerState.color.r * 255,
"g": playerState.color.g * 255,
"b": playerState.color.b * 255,
"a": playerState.color.a * 255
},
"team": playerState.team,
"teamsLocked": playerState.teamsLocked,
"cheatsEnabled": playerState.cheatsEnabled,
"state": playerState.state,
"isAlly": playerState.isAlly,
"isMutualAlly": playerState.isMutualAlly,
"isNeutral": playerState.isNeutral,
"isEnemy": playerState.isEnemy,
"guid": undefined, // network guid for players controlled by hosts
"offline": g_Players[i] && !!g_Players[i].offline
});
}
for (let guid in g_PlayerAssignments)
{
let playerID = g_PlayerAssignments[guid].player;
if (!playerData[playerID])
continue;
playerData[playerID].guid = guid;
playerData[playerID].name = g_PlayerAssignments[guid].name;
}
g_Players = playerData;
}
function updateDiplomacyColorsButton()
{
g_DiplomacyColorsToggle = !g_DiplomacyColorsToggle;
let diplomacyColorsButton = Engine.GetGUIObjectByName("diplomacyColorsButton");
diplomacyColorsButton.sprite = g_DiplomacyColorsToggle ?
"stretched:session/minimap-diplomacy-on.png" :
"stretched:session/minimap-diplomacy-off.png";
diplomacyColorsButton.sprite_over = g_DiplomacyColorsToggle ?
"stretched:session/minimap-diplomacy-on-highlight.png" :
"stretched:session/minimap-diplomacy-off-highlight.png";
Engine.GetGUIObjectByName("diplomacyColorsWindowButtonIcon").sprite = g_DiplomacyColorsToggle ?
"stretched:session/icons/diplomacy-on.png" :
"stretched:session/icons/diplomacy.png";
updateDisplayedPlayerColors();
}
/**
* Updates the displayed colors of players in the simulation and GUI.
*/
function updateDisplayedPlayerColors()
{
if (g_DiplomacyColorsToggle)
{
let teamRepresentatives = {};
for (let i = 1; i < g_Players.length; ++i)
if (g_ViewedPlayer <= 0)
{
// Observers and gaia see team colors
let team = g_Players[i].team;
g_DisplayedPlayerColors[i] = g_Players[teamRepresentatives[team] || i].color;
if (team != -1 && !teamRepresentatives[team])
teamRepresentatives[team] = i;
}
else
// Players see colors depending on diplomacy
g_DisplayedPlayerColors[i] =
g_ViewedPlayer == i ? g_DiplomacyColorPalette.Self :
g_Players[g_ViewedPlayer].isAlly[i] ? g_DiplomacyColorPalette.Ally :
g_Players[g_ViewedPlayer].isNeutral[i] ? g_DiplomacyColorPalette.Neutral :
g_DiplomacyColorPalette.Enemy;
g_DisplayedPlayerColors[0] = g_Players[0].color;
}
else
g_DisplayedPlayerColors = g_Players.map(player => player.color);
Engine.GuiInterfaceCall("UpdateDisplayedPlayerColors", {
"displayedPlayerColors": g_DisplayedPlayerColors,
"displayDiplomacyColors": g_DiplomacyColorsToggle,
"showAllStatusBars": g_ShowAllStatusBars,
"selected": g_Selection.toList()
});
updateGUIObjects();
}
/**
* Depends on the current player (g_IsObserver).
*/
function updateHotkeyTooltips()
{
Engine.GetGUIObjectByName("chatInput").tooltip =
translateWithContext("chat input", "Type the message to send.") + "\n" +
colorizeAutocompleteHotkey() +
colorizeHotkey("\n" + translate("Press %(hotkey)s to open the public chat."), "chat") +
colorizeHotkey(
"\n" + (g_IsObserver ?
translate("Press %(hotkey)s to open the observer chat.") :
translate("Press %(hotkey)s to open the ally chat.")),
"teamchat") +
colorizeHotkey("\n" + translate("Press %(hotkey)s to open the previously selected private chat."), "privatechat");
Engine.GetGUIObjectByName("idleWorkerButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "selection.idleworker") +
translate("Find idle worker");
Engine.GetGUIObjectByName("diplomacyColorsButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "session.diplomacycolors") +
translate("Toggle Diplomacy Colors");
Engine.GetGUIObjectByName("diplomacyColorsWindowButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "session.diplomacycolors") +
translate("Toggle Diplomacy Colors");
Engine.GetGUIObjectByName("tradeHelp").tooltip = colorizeHotkey(
translate("Select one type of goods you want to modify by clicking on it, and then use the arrows of the other types to modify their shares. You can also press %(hotkey)s while selecting one type of goods to bring its share to 100%%."),
"session.fulltradeswap");
Engine.GetGUIObjectByName("barterHelp").tooltip = sprintf(
translate("Start by selecting the resource you wish to sell from the upper row. For each time the lower buttons are pressed, %(quantity)s of the upper resource will be sold for the displayed quantity of the lower. Press and hold %(hotkey)s to temporarily multiply the traded amount by %(multiplier)s."), {
"quantity": g_BarterResourceSellQuantity,
"hotkey": colorizeHotkey("%(hotkey)s", "session.massbarter"),
"multiplier": g_BarterMultiplier
});
}
function initPanelEntities(slot)
{
let button = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]");
button.onPress = function() {
let panelEnt = g_PanelEntities.find(ent => ent.slot !== undefined && ent.slot == slot);
if (!panelEnt)
return;
if (!Engine.HotkeyIsPressed("selection.add"))
g_Selection.reset();
g_Selection.addList([panelEnt.ent]);
};
button.onDoublePress = function() {
let panelEnt = g_PanelEntities.find(ent => ent.slot !== undefined && ent.slot == slot);
if (panelEnt)
selectAndMoveTo(getEntityOrHolder(panelEnt.ent));
};
}
/**
* Returns the entity itself except when garrisoned where it returns its garrisonHolder
*/
function getEntityOrHolder(ent)
{
let entState = GetEntityState(ent);
if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length &&
(entState.unitAI.orders[0].type == "Garrison" || entState.unitAI.orders[0].type == "Autogarrison"))
return getEntityOrHolder(entState.unitAI.orders[0].data.target);
return ent;
}
function initializeMusic()
{
initMusic();
if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music)
global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music);
global.music.setState(global.music.states.PEACE);
playAmbient();
}
function updateViewedPlayerDropdown()
{
let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
viewPlayer.list_data = [-1].concat(g_Players.map((player, i) => i));
viewPlayer.list = [translate("Observer")].concat(g_Players.map(
(player, i) => colorizePlayernameHelper("■", i) + " " + player.name
));
}
function toggleChangePerspective(enabled)
{
g_DevSettings.changePerspective = enabled;
selectViewPlayer(g_ViewedPlayer);
}
/**
* Change perspective tool.
* Shown to observers or when enabling the developers option.
*/
function selectViewPlayer(playerID)
{
if (playerID < -1 || playerID > g_Players.length - 1)
return;
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay(true);
g_IsObserver = isPlayerObserver(Engine.GetPlayerID());
if (g_IsObserver || g_DevSettings.changePerspective)
{
if (g_ViewedPlayer != playerID)
clearSelection();
g_ViewedPlayer = playerID;
}
if (g_DevSettings.changePerspective)
{
Engine.SetPlayerID(g_ViewedPlayer);
g_IsObserver = isPlayerObserver(g_ViewedPlayer);
}
Engine.SetViewedPlayer(g_ViewedPlayer);
updateDisplayedPlayerColors();
updateTopPanel();
updateChatAddressees();
updateHotkeyTooltips();
// Update GUI and clear player-dependent cache
g_TemplateData = {};
Engine.GuiInterfaceCall("ResetTemplateModified");
onSimulationUpdate();
if (g_IsDiplomacyOpen)
openDiplomacy();
if (g_IsTradeOpen)
openTrade();
}
/**
* Returns true if the player with that ID is in observermode.
*/
function isPlayerObserver(playerID)
{
let playerStates = GetSimState().players;
return !playerStates[playerID] || playerStates[playerID].state != "active";
}
/**
* Returns true if the current user can issue commands for that player.
*/
function controlsPlayer(playerID)
{
let playerStates = GetSimState().players;
return playerStates[Engine.GetPlayerID()] &&
playerStates[Engine.GetPlayerID()].controlsAll ||
Engine.GetPlayerID() == playerID &&
playerStates[playerID] &&
playerStates[playerID].state != "defeated";
}
/**
* Called when one or more players have won or were defeated.
*
* @param {array} - IDs of the players who have won or were defeated.
* @param {object} - a plural string stating the victory reason.
* @param {boolean} - whether these players have won or lost.
*/
function playersFinished(players, victoryString, won)
{
addChatMessage({
"type": "defeat-victory",
"message": victoryString,
"players": players
});
if (players.indexOf(Engine.GetPlayerID()) != -1)
reportGame();
sendLobbyPlayerlistUpdate();
updatePlayerData();
updateChatAddressees();
updateGameSpeedControl();
if (players.indexOf(g_ViewedPlayer) == -1)
return;
// Select "observer" item on loss. On win enable observermode without changing perspective
Engine.GetGUIObjectByName("viewPlayer").selected = won ? g_ViewedPlayer + 1 : 0;
if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning())
return;
global.music.setState(
won ?
global.music.states.VICTORY :
global.music.states.DEFEAT
);
g_ConfirmExit = won ? "won" : "defeated";
}
/**
* Sets civ icon for the currently viewed player.
* Hides most gui objects for observers.
*/
function updateTopPanel()
{
let isPlayer = g_ViewedPlayer > 0;
let civIcon = Engine.GetGUIObjectByName("civIcon");
civIcon.hidden = !isPlayer;
if (isPlayer)
{
civIcon.sprite = "stretched:" + g_CivData[g_Players[g_ViewedPlayer].civ].Emblem;
Engine.GetGUIObjectByName("civIconOverlay").tooltip =
sprintf(
translate("%(civ)s\n%(hotkey_civinfo)s / %(hotkey_structree)s: View History / Structure Tree\nLast opened will be reopened on click."), {
"civ": setStringTags(g_CivData[g_Players[g_ViewedPlayer].civ].Name, { "font": "sans-bold-stroke-14" }),
"hotkey_civinfo": colorizeHotkey("%(hotkey)s", "civinfo"),
"hotkey_structree": colorizeHotkey("%(hotkey)s", "structree")
});
}
Engine.GetGUIObjectByName("optionFollowPlayer").hidden = !g_IsObserver ||
!isPlayer && (g_ViewedPlayer != 0 || g_GameAttributes.settings.Keywords.indexOf("trigger") == -1);
let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
viewPlayer.hidden = !g_IsObserver && !g_DevSettings.changePerspective;
let followPlayerLabel = Engine.GetGUIObjectByName("followPlayerLabel");
followPlayerLabel.hidden = Engine.GetTextWidth(followPlayerLabel.font, followPlayerLabel.caption + " ") +
followPlayerLabel.getComputedSize().left > viewPlayer.getComputedSize().left;
let resCodes = g_ResourceData.GetCodes();
let r = 0;
for (let res of resCodes)
{
if (!Engine.GetGUIObjectByName("resource[" + r + "]"))
{
warn("Current GUI limits prevent displaying more than " + r + " resources in the top panel!");
break;
}
Engine.GetGUIObjectByName("resource[" + r + "]_icon").sprite = "stretched:session/icons/resources/" + res + ".png";
Engine.GetGUIObjectByName("resource[" + r + "]").hidden = !isPlayer;
++r;
}
horizontallySpaceObjects("resourceCounts", 5);
hideRemaining("resourceCounts", r);
let resPop = Engine.GetGUIObjectByName("population");
let resPopSize = resPop.size;
resPopSize.left = Engine.GetGUIObjectByName("resource[" + (r - 1) + "]").size.right;
resPop.size = resPopSize;
Engine.GetGUIObjectByName("population").hidden = !isPlayer;
Engine.GetGUIObjectByName("diplomacyButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("tradeButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("observerText").hidden = isPlayer;
let alphaLabel = Engine.GetGUIObjectByName("alphaLabel");
alphaLabel.hidden = isPlayer && !viewPlayer.hidden;
alphaLabel.size = isPlayer ? "50%+44 0 100%-283 100%" : "155 0 85%-279 100%";
Engine.GetGUIObjectByName("pauseButton").enabled = !g_IsObserver || !g_IsNetworked || g_IsController;
Engine.GetGUIObjectByName("menuResignButton").enabled = !g_IsObserver;
Engine.GetGUIObjectByName("lobbyButton").enabled = Engine.HasXmppClient();
}
function reportPerformance(time)
{
let settings = g_GameAttributes.settings;
Engine.SubmitUserReport("profile", 3, JSON.stringify({
"time": time,
"map": settings.Name,
"seed": settings.Seed, // only defined for random maps
"size": settings.Size, // only defined for random maps
"profiler": Engine.GetProfilerState()
}));
}
/**
* Resign a player.
* @param leaveGameAfterResign If player is quitting after resignation.
*/
function resignGame(leaveGameAfterResign)
{
if (g_IsObserver || g_Disconnected)
return;
Engine.PostNetworkCommand({
"type": "resign"
});
if (!leaveGameAfterResign)
resumeGame(true);
}
/**
* Leave the game
* @param willRejoin If player is going to be rejoining a networked game.
*/
function leaveGame(willRejoin)
{
if (!willRejoin && !g_IsObserver)
resignGame(true);
// Before ending the game
let replayDirectory = Engine.GetCurrentReplayDirectory();
let simData = getReplayMetadata();
let playerID = Engine.GetPlayerID();
Engine.EndGame();
// After the replay file was closed in EndGame
// Done here to keep EndGame small
if (!g_IsReplay)
Engine.AddReplayToCache(replayDirectory);
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_summary.xml", {
"sim": simData,
"gui": {
"dialog": false,
"assignedPlayer": playerID,
"disconnected": g_Disconnected,
"isReplay": g_IsReplay,
"replayDirectory": !g_HasRejoined && replayDirectory,
"replaySelectionData": g_ReplaySelectionData
}
});
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return { "selection": g_Selection.selected };
}
function getSavedGameData()
{
return {
"groups": g_Groups.groups
};
}
function restoreSavedGameData(data)
{
// Restore camera if any
if (data.camera)
Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ,
data.camera.RotX, data.camera.RotY, data.camera.Zoom);
// Clear selection when loading a game
g_Selection.reset();
// Restore control groups
for (let groupNumber in data.groups)
{
g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
}
updateGroups();
}
/**
* Called every frame.
*/
function onTick()
{
if (!g_Settings)
return;
let now = Date.now();
let tickLength = now - g_LastTickTime;
g_LastTickTime = now;
handleNetMessages();
updateCursorAndTooltip();
if (g_Selection.dirty)
{
g_Selection.dirty = false;
// When selection changed, get the entityStates of new entities
GetMultipleEntityStates(g_Selection.toList().filter(entId => !g_EntityStates[entId]));
updateGUIObjects();
// Display rally points for selected buildings
if (Engine.GetPlayerID() != -1)
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength)
recalculateStatusBarDisplay();
updateTimers();
updateMenuPosition(tickLength);
// When training is blocked, flash population (alternates color every 500msec)
Engine.GetGUIObjectByName("resourcePop").textcolor = g_IsTrainingBlocked && now % 1000 < 500 ? g_PopulationAlertColor : g_DefaultPopulationColor;
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function onWindowResized()
{
// Update followPlayerLabel
updateTopPanel();
resizeChatWindow();
}
function changeGameSpeed(speed)
{
if (!g_IsNetworked)
Engine.SetSimRate(speed);
}
function updateIdleWorkerButton()
{
Engine.GetGUIObjectByName("idleWorkerButton").enabled = Engine.GuiInterfaceCall("HasIdleUnits", {
"viewedPlayer": g_ViewedPlayer,
"idleClasses": g_WorkerTypes,
"excludeUnits": []
});
}
function onSimulationUpdate()
{
// Templates change depending on technologies and auras, so they have to be reloaded after such a change.
// g_TechnologyData data never changes, so it shouldn't be deleted.
g_EntityStates = {};
if (Engine.GuiInterfaceCall("IsTemplateModified"))
{
g_TemplateData = {};
Engine.GuiInterfaceCall("ResetTemplateModified");
}
g_SimState = undefined;
if (!GetSimState())
return;
GetMultipleEntityStates(g_Selection.toList());
updateCinemaPath();
handleNotifications();
updateGUIObjects();
for (let type of ["Attack", "Auras", "Heal"])
Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", {
"type": type,
"enabled": Engine.ConfigDB_GetValue("user", "gui.session." + type.toLowerCase() + "range") == "true"
});
if (g_ConfirmExit)
confirmExit();
}
/**
* Don't show the message box before all playerstate changes are processed.
*/
function confirmExit()
{
if (g_IsNetworked && !g_IsNetworkedActive)
return;
closeOpenDialogs();
// Don't ask for exit if other humans are still playing
let isHost = g_IsController && g_IsNetworked;
let askExit = !isHost || isHost && g_Players.every((player, i) =>
i == 0 ||
player.state != "active" ||
g_GameAttributes.settings.PlayerData[i].AI != "");
let subject = g_PlayerStateMessages[g_ConfirmExit];
if (askExit)
subject += "\n" + translate("Do you want to quit?");
messageBox(
400, 200,
subject,
g_ConfirmExit == "won" ?
translate("VICTORIOUS!") :
translate("DEFEATED!"),
askExit ? [translate("No"), translate("Yes")] : [translate("OK")],
askExit ? [resumeGame, leaveGame] : [resumeGame]
);
g_ConfirmExit = false;
}
function updateCinemaPath()
{
let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected;
Engine.GetGUIObjectByName("sn").hidden = !g_ShowGUI || isPlayingCinemaPath;
Engine.Renderer_SetSilhouettesEnabled(!isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true");
}
function updateGUIObjects()
{
g_Selection.update();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updatePanelEntities();
displayPanelEntities();
updateGroups();
updateDebug();
updatePlayerDisplay();
updateResearchDisplay();
updateSelectionDetails();
updateBuildingPlacementPreview();
updateTimeNotifications();
updateIdleWorkerButton();
if (g_IsTradeOpen)
{
updateTraderTexts();
updateBarterButtons();
}
if (g_ViewedPlayer > 0)
{
let playerState = GetSimState().players[g_ViewedPlayer];
g_DevSettings.controlAll = playerState && playerState.controlsAll;
Engine.GetGUIObjectByName("devControlAll").checked = g_DevSettings.controlAll;
}
if (!g_IsObserver)
{
// Update music state on basis of battle state.
let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer);
if (battleState)
global.music.setState(global.music.states[battleState]);
}
updateViewedPlayerDropdown();
updateDiplomacy();
}
function onReplayFinished()
{
closeOpenDialogs();
pauseGame();
messageBox(400, 200,
translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"),
translateWithContext("replayFinished", "Confirmation"),
[translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")],
[resumeGame, leaveGame]);
}
/**
* updates a status bar on the GUI
* nameOfBar: name of the bar
* points: points to show
* maxPoints: max points
* direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3;
*/
function updateGUIStatusBar(nameOfBar, points, maxPoints, direction)
{
// check, if optional direction parameter is valid.
if (!direction || !(direction >= 0 && direction < 4))
direction = 0;
// get the bar and update it
let statusBar = Engine.GetGUIObjectByName(nameOfBar);
if (!statusBar)
return;
let healthSize = statusBar.size;
let value = 100 * Math.max(0, Math.min(1, points / maxPoints));
// inverse bar
if (direction == 2 || direction == 3)
value = 100 - value;
if (direction == 0)
healthSize.rright = value;
else if (direction == 1)
healthSize.rbottom = value;
else if (direction == 2)
healthSize.rleft = value;
else if (direction == 3)
healthSize.rtop = value;
statusBar.size = healthSize;
}
function updatePanelEntities()
{
let panelEnts =
g_ViewedPlayer == -1 ?
GetSimState().players.reduce((ents, pState) => ents.concat(pState.panelEntities), []) :
GetSimState().players[g_ViewedPlayer].panelEntities;
g_PanelEntities = g_PanelEntities.filter(panelEnt => panelEnts.find(ent => ent == panelEnt.ent));
for (let ent of panelEnts)
{
let panelEntState = GetEntityState(ent);
let template = GetTemplateData(panelEntState.template);
let panelEnt = g_PanelEntities.find(pEnt => ent == pEnt.ent);
if (!panelEnt)
{
panelEnt = {
"ent": ent,
"tooltip": undefined,
"sprite": "stretched:session/portraits/" + template.icon,
"maxHitpoints": undefined,
"currentHitpoints": panelEntState.hitpoints,
"previousHitpoints": undefined
};
g_PanelEntities.push(panelEnt);
}
panelEnt.tooltip = createPanelEntityTooltip(panelEntState, template);
panelEnt.previousHitpoints = panelEnt.currentHitpoints;
panelEnt.currentHitpoints = panelEntState.hitpoints;
panelEnt.maxHitpoints = panelEntState.maxHitpoints;
}
let panelEntIndex = ent => g_PanelEntityOrder.findIndex(entClass =>
GetEntityState(ent).identity.classes.indexOf(entClass) != -1);
g_PanelEntities = g_PanelEntities.sort((panelEntA, panelEntB) => panelEntIndex(panelEntA.ent) - panelEntIndex(panelEntB.ent));
}
function createPanelEntityTooltip(panelEntState, template)
{
let getPanelEntNameTooltip = panelEntState => "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]";
return [
getPanelEntNameTooltip,
getCurrentHealthTooltip,
getAttackTooltip,
getArmorTooltip,
getEntityTooltip,
getAurasTooltip
].map(tooltip => tooltip(panelEntState)).filter(tip => tip).join("\n");
}
function displayPanelEntities()
{
let buttons = Engine.GetGUIObjectByName("panelEntityPanel").children;
buttons.forEach((button, slot) => {
if (button.hidden || g_PanelEntities.some(ent => ent.slot !== undefined && ent.slot == slot))
return;
button.hidden = true;
stopColorFade("panelEntityHitOverlay[" + slot + "]");
});
// The slot identifies the button, displayIndex determines its position.
for (let displayIndex = 0; displayIndex < Math.min(g_PanelEntities.length, buttons.length); ++displayIndex)
{
let panelEnt = g_PanelEntities[displayIndex];
// Find the first unused slot if new, otherwise reuse previous.
let slot = panelEnt.slot === undefined ?
buttons.findIndex(button => button.hidden) :
panelEnt.slot;
let panelEntButton = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]");
panelEntButton.tooltip = panelEnt.tooltip;
updateGUIStatusBar("panelEntityHealthBar[" + slot + "]", panelEnt.currentHitpoints, panelEnt.maxHitpoints);
if (panelEnt.slot === undefined)
{
let panelEntImage = Engine.GetGUIObjectByName("panelEntityImage[" + slot + "]");
panelEntImage.sprite = panelEnt.sprite;
panelEntButton.hidden = false;
panelEnt.slot = slot;
}
// If the health of the panelEnt changed since the last update, trigger the animation.
if (panelEnt.previousHitpoints > panelEnt.currentHitpoints)
startColorFade("panelEntityHitOverlay[" + slot + "]", 100, 0,
colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit);
// TODO: Instead of instant position changes, animate button movement.
setPanelObjectPosition(panelEntButton, displayIndex, buttons.length);
}
}
function updateGroups()
{
g_Groups.update();
// Determine the sum of the costs of a given template
let getCostSum = (ent) => {
let cost = GetTemplateData(GetEntityState(ent).template).cost;
return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0;
};
for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children)
{
Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i;
let button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]");
button.hidden = g_Groups.groups[i].getTotalCount() == 0;
button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i);
button.ondoublepress = (function(i) { return function() { performGroup("snap", i); }; })(i);
button.onpressright = (function(i) { return function() { performGroup("breakUp", i); }; })(i);
// Chose icon of the most common template (or the most costly if it's not unique)
if (g_Groups.groups[i].getTotalCount() > 0)
{
let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => {
if (pre.ents.length == cur.ents.length)
return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur;
return pre.ents.length > cur.ents.length ? pre : cur;
}).ents[0]).template).icon;
Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite =
icon ? ("stretched:session/portraits/" + icon) : "groupsIcon";
}
setPanelObjectPosition(button, i, 1);
}
}
function updateDebug()
{
let debug = Engine.GetGUIObjectByName("debugEntityState");
if (!Engine.GetGUIObjectByName("devDisplayState").checked)
{
debug.hidden = true;
return;
}
debug.hidden = false;
let conciseSimState = clone(GetSimState());
conciseSimState.players = "<<>>";
let text = "simulation: " + uneval(conciseSimState);
let selection = g_Selection.toList();
if (selection.length)
{
let entState = GetEntityState(selection[0]);
if (entState)
{
let template = GetTemplateData(entState.template);
text += "\n\nentity: {\n";
for (let k in entState)
text += " " + k + ":" + uneval(entState[k]) + "\n";
text += "}\n\ntemplate: " + uneval(template);
}
}
debug.caption = text.replace(/\[/g, "\\[");
}
/**
* Create ally player stat tooltip.
* @param {string} resource - Resource type, on which values will be sorted.
* @param {object} playerStates - Playerstates from players whos stats are viewed in the tooltip.
* @param {number} sort - 0 no order, -1 descending, 1 ascending order.
* @returns {string} Tooltip string.
*/
function getAllyStatTooltip(resource, playerStates, sort)
{
let tooltip = [];
for (let player in playerStates)
tooltip.push({
"playername": colorizePlayernameHelper("■", player) + " " + g_Players[player].name,
"statValue": resource == "pop" ?
sprintf(translate("%(popCount)s/%(popLimit)s/%(popMax)s"), playerStates[player]) :
Math.round(playerStates[player].resourceCounts[resource]),
"orderValue": resource == "pop" ? playerStates[player].popCount :
Math.round(playerStates[player].resourceCounts[resource])
});
if (sort)
tooltip.sort((a, b) => sort * (b.orderValue - a.orderValue));
return "\n" + tooltip.map(stat => sprintf(translate("%(playername)s: %(statValue)s"), stat)).join("\n");
}
function updatePlayerDisplay()
{
let allPlayerStates = GetSimState().players;
let viewedPlayerState = allPlayerStates[g_ViewedPlayer];
let viewablePlayerStates = {};
for (let player in allPlayerStates)
if (player != 0 &&
player != g_ViewedPlayer &&
g_Players[player].state != "defeated" &&
(g_IsObserver ||
viewedPlayerState.hasSharedLos &&
g_Players[player].isMutualAlly[g_ViewedPlayer]))
viewablePlayerStates[player] = allPlayerStates[player];
if (!viewedPlayerState)
return;
let tooltipSort = +Engine.ConfigDB_GetValue("user", "gui.session.respoptooltipsort");
let orderHotkeyTooltip = Object.keys(viewablePlayerStates).length <= 1 ? "" :
"\n" + sprintf(translate("%(order)s: %(hotkey)s to change order."), {
"hotkey": setStringTags("\\[Click]", g_HotkeyTags),
"order": tooltipSort == 0 ? translate("Unordered") : tooltipSort == 1 ? translate("Descending") : translate("Ascending")
});
let resCodes = g_ResourceData.GetCodes();
for (let r = 0; r < resCodes.length; ++r)
{
let resourceObj = Engine.GetGUIObjectByName("resource[" + r + "]");
if (!resourceObj)
break;
let res = resCodes[r];
let tooltip = '[font="' + g_ResourceTitleFont + '"]' +
resourceNameFirstWord(res) + '[/font]';
let descr = g_ResourceData.GetResource(res).description;
if (descr)
tooltip += "\n" + translate(descr);
tooltip += orderHotkeyTooltip + getAllyStatTooltip(res, viewablePlayerStates, tooltipSort);
resourceObj.tooltip = tooltip;
Engine.GetGUIObjectByName("resource[" + r + "]_count").caption = Math.floor(viewedPlayerState.resourceCounts[res]);
}
Engine.GetGUIObjectByName("resourcePop").caption = sprintf(translate("%(popCount)s/%(popLimit)s"), viewedPlayerState);
Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" +
sprintf(translate("Maximum population: %(popCap)s"), { "popCap": viewedPlayerState.popMax }) +
orderHotkeyTooltip +
getAllyStatTooltip("pop", viewablePlayerStates, tooltipSort);
g_IsTrainingBlocked = viewedPlayerState.trainingBlocked;
}
function selectAndMoveTo(ent)
{
let entState = GetEntityState(ent);
if (!entState || !entState.position)
return;
g_Selection.reset();
g_Selection.addList([ent]);
let position = entState.position;
Engine.CameraMoveTo(position.x, position.z);
}
function updateResearchDisplay()
{
let researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", g_ViewedPlayer);
// Set up initial positioning.
let buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right;
for (let i = 0; i < 10; ++i)
{
let button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]");
let size = button.size;
size.top = g_ResearchListTop + (4 + buttonSideLength) * i;
size.bottom = size.top + buttonSideLength;
button.size = size;
}
let numButtons = 0;
for (let tech in researchStarted)
{
// Show at most 10 in-progress techs.
if (numButtons >= 10)
break;
let template = GetTechnologyData(tech, g_Players[g_ViewedPlayer].civ);
let button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]");
button.hidden = false;
button.tooltip = getEntityNames(template);
button.onpress = (function(e) { return function() { selectAndMoveTo(e); }; })(researchStarted[tech].researcher);
let icon = "stretched:session/portraits/" + template.icon;
Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon;
// Scale the progress indicator.
let size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left));
Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size;
Engine.GetGUIObjectByName("researchStartedTimeRemaining[" + numButtons + "]").caption =
Engine.FormatMillisecondsIntoDateStringGMT(researchStarted[tech].timeRemaining, translateWithContext("countdown format", "m:ss"));
++numButtons;
}
// Hide unused buttons.
for (let i = numButtons; i < 10; ++i)
Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true;
}
/**
* Toggles the display of status bars for all of the player's entities.
*
* @param {Boolean} remove - Whether to hide all previously shown status bars.
*/
function recalculateStatusBarDisplay(remove = false)
{
let entities;
if (g_ShowAllStatusBars && !remove)
entities = g_ViewedPlayer == -1 ?
Engine.PickNonGaiaEntitiesOnScreen() :
Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer);
else
{
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
// Remove selected entities from the 'all entities' array,
// to avoid disabling their status bars.
entities = Engine.GuiInterfaceCall(
g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", {
"viewedPlayer": g_ViewedPlayer
}).filter(idx => selected.indexOf(idx) == -1);
}
Engine.GuiInterfaceCall("SetStatusBars", {
"entities": entities,
"enabled": g_ShowAllStatusBars && !remove,
"showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true"
});
}
/**
* Inverts the given configuration boolean and returns the current state.
* For example "silhouettes".
*/
function toggleConfigBool(configName)
{
let enabled = Engine.ConfigDB_GetValue("user", configName) != "true";
saveSettingAndWriteToUserConfig(configName, String(enabled));
return enabled;
}
/**
* Toggles the display of range overlays of selected entities for the given range type.
* @param {string} type - for example "Auras"
*/
function toggleRangeOverlay(type)
{
let enabled = toggleConfigBool("gui.session." + type.toLowerCase() + "range");
Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", {
"type": type,
"enabled": enabled
});
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
Engine.GuiInterfaceCall("SetRangeOverlays", {
"entities": selected,
"enabled": enabled
});
}
// Update the additional list of entities to be highlighted.
function updateAdditionalHighlight()
{
let entsAdd = []; // list of entities units to be highlighted
let entsRemove = [];
let highlighted = g_Selection.toList();
for (let ent in g_Selection.highlighted)
highlighted.push(g_Selection.highlighted[ent]);
if (g_ShowGuarding)
// flag the guarding entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.guard || !state.guard.entities.length)
continue;
for (let ent of state.guard.entities)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
if (g_ShowGuarded)
// flag the guarded entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.unitAI || !state.unitAI.isGuarding)
continue;
let ent = state.unitAI.isGuarding;
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
// flag the entities to remove (from the previously added) from this additional highlight
for (let ent of g_AdditionalHighlight)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
entsRemove.push(ent);
_setHighlight(entsAdd, g_HighlightedAlpha, true);
_setHighlight(entsRemove, 0, false);
g_AdditionalHighlight = entsAdd;
}
function playAmbient()
{
Engine.PlayAmbientSound(pickRandom(g_Ambient), true);
}
function getBuildString()
{
return sprintf(translate("Build: %(buildDate)s (%(revision)s)"), {
"buildDate": Engine.GetBuildTimestamp(0),
"revision": Engine.GetBuildTimestamp(2)
});
}
function showTimeWarpMessageBox()
{
messageBox(
500, 250,
translate("Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."),
translate("Time warp mode")
);
}
/**
* Adds the ingame time and ceasefire counter to the global FPS and
* realtime counters shown in the top right corner.
*/
function appendSessionCounters(counters)
{
let simState = GetSimState();
if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true")
{
let currentSpeed = Engine.GetSimRate();
if (currentSpeed != 1.0)
// Translation: The "x" means "times", with the mathematical meaning of multiplication.
counters.push(sprintf(translate("%(time)s (%(speed)sx)"), {
"time": timeToString(simState.timeElapsed),
"speed": Engine.FormatDecimalNumberIntoString(currentSpeed)
}));
else
counters.push(timeToString(simState.timeElapsed));
}
if (simState.ceasefireActive && Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") === "true")
counters.push(timeToString(simState.ceasefireTimeRemaining));
g_ResearchListTop = 4 + 14 * counters.length;
}
/**
* Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
function sendLobbyPlayerlistUpdate()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
// Extract the relevant player data and minimize packet load
let minPlayerData = [];
for (let playerID in g_GameAttributes.settings.PlayerData)
{
if (+playerID == 0)
continue;
let pData = g_GameAttributes.settings.PlayerData[playerID];
let minPData = { "Name": pData.Name, "Civ": pData.Civ };
if (g_GameAttributes.settings.LockTeams)
minPData.Team = pData.Team;
if (pData.AI)
{
minPData.AI = pData.AI;
minPData.AIDiff = pData.AIDiff;
minPData.AIBehavior = pData.AIBehavior;
}
if (g_Players[playerID].offline)
minPData.Offline = true;
// Whether the player has won or was defeated
let state = g_Players[playerID].state;
if (state != "active")
minPData.State = state;
minPlayerData.push(minPData);
}
// Add observers
let connectedPlayers = 0;
for (let guid in g_PlayerAssignments)
{
let pData = g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player];
if (pData)
++connectedPlayers;
else
minPlayerData.push({
"Name": g_PlayerAssignments[guid].name,
"Team": "observer"
});
}
Engine.SendChangeStateGame(connectedPlayers, playerDataToStringifiedTeamList(minPlayerData));
}
/**
* Send a report on the gamestatus to the lobby.
*/
function reportGame()
{
// Only 1v1 games are rated (and Gaia is part of g_Players)
if (!Engine.HasXmppClient() || !Engine.IsRankedGame() ||
g_Players.length != 3 || Engine.GetPlayerID() == -1)
return;
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
let unitsClasses = [
"total",
"Infantry",
"Worker",
"FemaleCitizen",
"Cavalry",
"Champion",
"Hero",
"Siege",
"Ship",
"Trader"
];
let unitsCountersTypes = [
"unitsTrained",
"unitsLost",
"enemyUnitsKilled"
];
let buildingsClasses = [
"total",
"CivCentre",
"House",
"Economic",
"Outpost",
"Military",
"Fortress",
"Wonder"
];
let buildingsCountersTypes = [
"buildingsConstructed",
"buildingsLost",
"enemyBuildingsDestroyed"
];
let resourcesTypes = [
"wood",
"food",
"stone",
"metal"
];
let resourcesCounterTypes = [
"resourcesGathered",
"resourcesUsed",
"resourcesSold",
"resourcesBought"
];
let misc = [
"tradeIncome",
"tributesSent",
"tributesReceived",
"treasuresCollected",
"lootCollected",
"percentMapExplored"
];
let playerStatistics = {};
// Unit Stats
for (let unitCounterType of unitsCountersTypes)
{
if (!playerStatistics[unitCounterType])
playerStatistics[unitCounterType] = { };
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] = "";
}
playerStatistics.unitsLostValue = "";
playerStatistics.unitsKilledValue = "";
// Building stats
for (let buildingCounterType of buildingsCountersTypes)
{
if (!playerStatistics[buildingCounterType])
playerStatistics[buildingCounterType] = { };
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] = "";
}
playerStatistics.buildingsLostValue = "";
playerStatistics.enemyBuildingsDestroyedValue = "";
// Resources
for (let resourcesCounterType of resourcesCounterTypes)
{
if (!playerStatistics[resourcesCounterType])
playerStatistics[resourcesCounterType] = { };
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] = "";
}
playerStatistics.resourcesGathered.vegetarianFood = "";
for (let type of misc)
playerStatistics[type] = "";
// Total
playerStatistics.economyScore = "";
playerStatistics.militaryScore = "";
playerStatistics.totalScore = "";
let mapName = g_GameAttributes.settings.Name;
let playerStates = "";
let playerCivs = "";
let teams = "";
let teamsLocked = true;
// Serialize the statistics for each player into a comma-separated list.
// Ignore gaia
for (let i = 1; i < extendedSimState.players.length; ++i)
{
let player = extendedSimState.players[i];
let maxIndex = player.sequences.time.length - 1;
playerStates += player.state + ",";
playerCivs += player.civ + ",";
teams += player.team + ",";
teamsLocked = teamsLocked && player.teamsLocked;
for (let resourcesCounterType of resourcesCounterTypes)
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] += player.sequences[resourcesCounterType][resourcesType][maxIndex] + ",";
playerStatistics.resourcesGathered.vegetarianFood += player.sequences.resourcesGathered.vegetarianFood[maxIndex] + ",";
for (let unitCounterType of unitsCountersTypes)
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] += player.sequences[unitCounterType][unitsClass][maxIndex] + ",";
for (let buildingCounterType of buildingsCountersTypes)
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] += player.sequences[buildingCounterType][buildingsClass][maxIndex] + ",";
let total = 0;
for (let type in player.sequences.resourcesGathered)
total += player.sequences.resourcesGathered[type][maxIndex];
playerStatistics.economyScore += total + ",";
playerStatistics.militaryScore += Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] +
player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10) + ",";
playerStatistics.totalScore += (total + Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] +
player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10)) + ",";
for (let type of misc)
playerStatistics[type] += player.sequences[type][maxIndex] + ",";
}
// Send the report with serialized data
let reportObject = {};
reportObject.timeElapsed = extendedSimState.timeElapsed;
reportObject.playerStates = playerStates;
reportObject.playerID = Engine.GetPlayerID();
reportObject.matchID = g_GameAttributes.matchID;
reportObject.civs = playerCivs;
reportObject.teams = teams;
reportObject.teamsLocked = String(teamsLocked);
reportObject.ceasefireActive = String(extendedSimState.ceasefireActive);
reportObject.ceasefireTimeRemaining = String(extendedSimState.ceasefireTimeRemaining);
reportObject.mapName = mapName;
reportObject.economyScore = playerStatistics.economyScore;
reportObject.militaryScore = playerStatistics.militaryScore;
reportObject.totalScore = playerStatistics.totalScore;
for (let rct of resourcesCounterTypes)
for (let rt of resourcesTypes)
reportObject[rt + rct.substr(9)] = playerStatistics[rct][rt];
// eg. rt = food rct.substr = Gathered rct = resourcesGathered
reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood;
for (let type of unitsClasses)
{
// eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "UnitsTrained"] = playerStatistics.unitsTrained[type];
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "UnitsLost"] = playerStatistics.unitsLost[type];
reportObject["enemy" + type + "UnitsKilled"] = playerStatistics.enemyUnitsKilled[type];
}
for (let type of buildingsClasses)
{
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "BuildingsConstructed"] = playerStatistics.buildingsConstructed[type];
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "BuildingsLost"] = playerStatistics.buildingsLost[type];
reportObject["enemy" + type + "BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type];
}
for (let type of misc)
reportObject[type] = playerStatistics[type];
Engine.SendGameReport(reportObject);
}
Index: ps/trunk/binaries/data/mods/public/maps/random/wall_demo.json
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/wall_demo.json (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/random/wall_demo.json (revision 21474)
@@ -1,13 +1,13 @@
{
"settings" : {
"Name" : "Wall Demo",
- "GameType" : "endless",
"Script" : "wall_demo.js",
"Description" : "A demonstration of wall placement methods/code in random maps. Giant map size is recommended!",
"Keywords": ["demo"],
"CircularMap" : false,
"TriggerScripts" : [
"random/wall_demo_triggers.js"
- ]
+ ],
+ "VictoryConditions": []
}
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (revision 21474)
@@ -1,191 +1,191 @@
Trigger.prototype.InitCaptureTheRelic = function()
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let catafalqueTemplates = shuffleArray(cmpTemplateManager.FindAllTemplates(false).filter(
name => GetIdentityClasses(cmpTemplateManager.GetTemplate(name).Identity || {}).indexOf("Relic") != -1));
let potentialSpawnPoints = TriggerHelper.GetLandSpawnPoints();
if (!potentialSpawnPoints.length)
{
error("No gaia entities found on this map that could be used as spawn points!");
return;
}
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
- let numSpawnedRelics = cmpEndGameManager.GetGameTypeSettings().relicCount;
+ let numSpawnedRelics = cmpEndGameManager.GetGameSettings().relicCount;
this.playerRelicsCount = new Array(TriggerHelper.GetNumberOfPlayers()).fill(0, 1);
this.playerRelicsCount[0] = numSpawnedRelics;
for (let i = 0; i < numSpawnedRelics; ++i)
{
this.relics[i] = TriggerHelper.SpawnUnits(pickRandom(potentialSpawnPoints), catafalqueTemplates[i], 1, 0)[0];
let cmpDamageReceiver = Engine.QueryInterface(this.relics[i], IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(true);
let cmpPositionRelic = Engine.QueryInterface(this.relics[i], IID_Position);
cmpPositionRelic.SetYRotation(randomAngle());
}
};
Trigger.prototype.CheckCaptureTheRelicVictory = function(data)
{
let cmpIdentity = Engine.QueryInterface(data.entity, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass("Relic") || data.from == INVALID_PLAYER)
return;
--this.playerRelicsCount[data.from];
if (data.to == -1)
{
warn("Relic entity " + data.entity + " has been destroyed");
this.relics.splice(this.relics.indexOf(data.entity), 1);
}
else
++this.playerRelicsCount[data.to];
this.DeleteCaptureTheRelicVictoryMessages();
this.CheckCaptureTheRelicCountdown();
};
/**
* Check if a group of mutually allied players have acquired all relics.
* The winning players are the relic owners and all players mutually allied to all relic owners.
* Reset the countdown if the group of winning players changes or extends.
*/
Trigger.prototype.CheckCaptureTheRelicCountdown = function()
{
if (this.playerRelicsCount[0])
{
this.DeleteCaptureTheRelicVictoryMessages();
return;
}
let activePlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNonGaiaPlayers().filter(
playerID => QueryPlayerIDInterface(playerID).GetState() == "active");
let relicOwners = activePlayers.filter(playerID => this.playerRelicsCount[playerID]);
if (!relicOwners.length)
{
this.DeleteCaptureTheRelicVictoryMessages();
return;
}
let winningPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager).GetAlliedVictory() ?
activePlayers.filter(playerID => relicOwners.every(owner => QueryPlayerIDInterface(playerID).IsMutualAlly(owner))) :
[relicOwners[0]];
// All relicOwners should be mutually allied
if (relicOwners.some(owner => winningPlayers.indexOf(owner) == -1))
{
this.DeleteCaptureTheRelicVictoryMessages();
return;
}
// Reset the timer when playerAndAllies isn't the same as this.relicsVictoryCountdownPlayers
if (winningPlayers.length != this.relicsVictoryCountdownPlayers.length ||
winningPlayers.some(player => this.relicsVictoryCountdownPlayers.indexOf(player) == -1))
{
this.relicsVictoryCountdownPlayers = winningPlayers;
this.StartCaptureTheRelicCountdown(winningPlayers);
}
};
Trigger.prototype.DeleteCaptureTheRelicVictoryMessages = function()
{
if (!this.relicsVictoryTimer)
return;
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).CancelTimer(this.relicsVictoryTimer);
this.relicsVictoryTimer = undefined;
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.DeleteTimeNotification(this.ownRelicsVictoryMessage);
cmpGuiInterface.DeleteTimeNotification(this.othersRelicsVictoryMessage);
this.relicsVictoryCountdownPlayers = [];
};
Trigger.prototype.StartCaptureTheRelicCountdown = function(winningPlayers)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
if (this.relicsVictoryTimer)
{
cmpTimer.CancelTimer(this.relicsVictoryTimer);
cmpGuiInterface.DeleteTimeNotification(this.ownRelicsVictoryMessage);
cmpGuiInterface.DeleteTimeNotification(this.othersRelicsVictoryMessage);
}
if (!this.relics.length)
return;
let others = [-1];
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
{
let cmpPlayer = QueryPlayerIDInterface(playerID);
if (cmpPlayer.GetState() == "won")
return;
if (winningPlayers.indexOf(playerID) == -1)
others.push(playerID);
}
let cmpPlayer = QueryOwnerInterface(this.relics[0], IID_Player);
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
- let captureTheRelicDuration = cmpEndGameManager.GetGameTypeSettings().relicDuration;
+ let captureTheRelicDuration = cmpEndGameManager.GetGameSettings().relicDuration;
let isTeam = winningPlayers.length > 1;
this.ownRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({
"message": isTeam ?
markForTranslation("%(_player_)s and their allies have captured all relics and will win in %(time)s.") :
markForTranslation("%(_player_)s has captured all relics and will win in %(time)s."),
"players": others,
"parameters": {
"_player_": cmpPlayer.GetPlayerID()
},
"translateMessage": true,
"translateParameters": []
}, captureTheRelicDuration);
this.othersRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({
"message": isTeam ?
markForTranslation("You and your allies have captured all relics and will win in %(time)s.") :
markForTranslation("You have captured all relics and will win in %(time)s."),
"players": winningPlayers,
"translateMessage": true
}, captureTheRelicDuration);
this.relicsVictoryTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger,
"CaptureTheRelicVictorySetWinner", captureTheRelicDuration, winningPlayers);
};
Trigger.prototype.CaptureTheRelicVictorySetWinner = function(winningPlayers)
{
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
cmpEndGameManager.MarkPlayersAsWon(
winningPlayers,
n => markForPluralTranslation(
"%(lastPlayer)s has won (Capture the Relic).",
"%(players)s and %(lastPlayer)s have won (Capture the Relic).",
n),
n => markForPluralTranslation(
"%(lastPlayer)s has been defeated (Capture the Relic).",
"%(players)s and %(lastPlayer)s have been defeated (Capture the Relic).",
n));
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.relics = [];
cmpTrigger.playerRelicsCount = [];
cmpTrigger.relicsVictoryTimer = undefined;
cmpTrigger.ownRelicsVictoryMessage = undefined;
cmpTrigger.othersRelicsVictoryMessage = undefined;
cmpTrigger.relicsVictoryCountdownPlayers = [];
cmpTrigger.DoAfterDelay(0, "InitCaptureTheRelic", {});
cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "CheckCaptureTheRelicCountdown", { "enabled": true });
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckCaptureTheRelicVictory", { "enabled": true });
cmpTrigger.RegisterTrigger("OnPlayerWon", "DeleteCaptureTheRelicVictoryMessages", { "enabled": true });
cmpTrigger.RegisterTrigger("OnPlayerDefeated", "CheckCaptureTheRelicCountdown", { "enabled": true });
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/Conquest.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/Conquest.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/Conquest.js (revision 21474)
@@ -1,5 +1,7 @@
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
- cmpTrigger.conquestClassFilter = "ConquestCritical";
- cmpTrigger.conquestDefeatReason = markForTranslation("%(player)s has been defeated (lost all critical units and structures).");
+ cmpTrigger.ConquestAddVictoryCondition({
+ "classFilter": "ConquestCritical",
+ "defeatReason": markForTranslation("%(player)s has been defeated (lost all critical units and structures).")
+ });
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/ConquestCommon.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/ConquestCommon.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/ConquestCommon.js (revision 21474)
@@ -1,54 +1,62 @@
Trigger.prototype.ConquestOwnershipChanged = function(msg)
{
- if (!this.conquestDataInit || !this.conquestClassFilter)
+ if (!this.conquestDataInit)
return;
- if (!TriggerHelper.EntityMatchesClassList(msg.entity, this.conquestClassFilter))
- return;
+ for (let query of this.conquestQueries)
+ {
+ if (!TriggerHelper.EntityMatchesClassList(msg.entity, query.classFilter))
+ continue;
- if (msg.to > 0)
- this.conquestEntitiesByPlayer[msg.to].push(msg.entity);
+ if (msg.to > 0)
+ query.entitiesByPlayer[msg.to].push(msg.entity);
- if (msg.from > 0)
- {
- let entities = this.conquestEntitiesByPlayer[msg.from];
+ if (msg.from <= 0)
+ continue;
+
+ let entities = query.entitiesByPlayer[msg.from];
let index = entities.indexOf(msg.entity);
if (index != -1)
entities.splice(index, 1);
if (!entities.length)
{
let cmpPlayer = QueryPlayerIDInterface(msg.from);
if (cmpPlayer)
- cmpPlayer.SetState("defeated", this.conquestDefeatReason);
+ cmpPlayer.SetState("defeated", query.defeatReason);
}
}
};
Trigger.prototype.ConquestStartGameCount = function()
{
- if (!this.conquestClassFilter)
+ if (!this.conquestQueries.length)
{
- warn("ConquestStartGameCount: conquestClassFilter undefined");
+ warn("ConquestStartGameCount: no conquestQueries set");
return;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ let entitiesByPlayer = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers().map(playerID =>
+ cmpRangeManager.GetEntitiesByPlayer(playerID));
- let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
- for (let i = 1; i < numPlayers; ++i)
- this.conquestEntitiesByPlayer[i] =
- cmpRangeManager.GetEntitiesByPlayer(i).filter(ent =>
- TriggerHelper.EntityMatchesClassList(ent, this.conquestClassFilter));
+ for (let query of this.conquestQueries)
+ query.entitiesByPlayer = entitiesByPlayer.map(
+ ents => ents.filter(
+ ent => TriggerHelper.EntityMatchesClassList(ent, query.classFilter)));
this.conquestDataInit = true;
};
+Trigger.prototype.ConquestAddVictoryCondition = function(data)
+{
+ this.conquestQueries.push(data);
+};
+
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
- cmpTrigger.conquestEntitiesByPlayer = {};
cmpTrigger.conquestDataInit = false;
- cmpTrigger.conquestClassFilter = "";
+ cmpTrigger.conquestQueries = [];
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "ConquestOwnershipChanged", { "enabled": true });
cmpTrigger.DoAfterDelay(0, "ConquestStartGameCount", null);
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/ConquestStructures.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/ConquestStructures.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/ConquestStructures.js (revision 21474)
@@ -1,5 +1,7 @@
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
- cmpTrigger.conquestClassFilter = "Structure";
- cmpTrigger.conquestDefeatReason = markForTranslation("%(player)s has been defeated (lost all structures).");
+ cmpTrigger.ConquestAddVictoryCondition({
+ "classFilter": "Structure",
+ "defeatReason": markForTranslation("%(player)s has been defeated (lost all structures).")
+ });
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/ConquestUnits.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/ConquestUnits.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/ConquestUnits.js (revision 21474)
@@ -1,5 +1,7 @@
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
- cmpTrigger.conquestClassFilter = "Unit+!Animal";
- cmpTrigger.conquestDefeatReason = markForTranslation("%(player)s has been defeated (lost all units).");
+ cmpTrigger.ConquestAddVictoryCondition({
+ "classFilter": "Unit+!Animal",
+ "defeatReason": markForTranslation("%(player)s has been defeated (lost all units).")
+ });
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js (revision 21474)
@@ -1,131 +1,131 @@
Trigger.prototype.CheckRegicideDefeat = function(data)
{
if (data.entity == this.regicideHeroes[data.from])
TriggerHelper.DefeatPlayer(
data.from,
markForTranslation("%(player)s has been defeated (lost hero)."));
};
Trigger.prototype.InitRegicideGame = function(msg)
{
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
- let regicideGarrison = cmpEndGameManager.GetGameTypeSettings().regicideGarrison;
+ let regicideGarrison = cmpEndGameManager.GetGameSettings().regicideGarrison;
let playersCivs = [];
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
playersCivs[playerID] = QueryPlayerIDInterface(playerID).GetCiv();
// Get all hero templates of these civs
let heroTemplates = {};
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let templateName of cmpTemplateManager.FindAllTemplates(false))
{
if (!templateName.startsWith("units/"))
continue;
let identity = cmpTemplateManager.GetTemplate(templateName).Identity;
let classes = GetIdentityClasses(identity);
if (classes.indexOf("Hero") == -1 ||
playersCivs.every(civ => civ != identity.Civ))
continue;
if (!heroTemplates[identity.Civ])
heroTemplates[identity.Civ] = [];
if (heroTemplates[identity.Civ].indexOf(templateName) == -1)
heroTemplates[identity.Civ].push({
"templateName": regicideGarrison ? templateName : "ungarrisonable|" + templateName,
"classes": classes
});
}
// Sort available spawn points by preference
let spawnPreferences = ["CivilCentre", "Structure", "Ship"];
let getSpawnPreference = entity => {
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
if (!cmpIdentity)
return -1;
let classes = cmpIdentity.GetClassesList();
for (let i in spawnPreferences)
if (classes.indexOf(spawnPreferences[i]) != -1)
return spawnPreferences.length - i;
return 0;
};
// Attempt to spawn one hero per player
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
{
let spawnPoints = cmpRangeManager.GetEntitiesByPlayer(playerID).sort((entity1, entity2) =>
getSpawnPreference(entity2) - getSpawnPreference(entity1));
// Spawn the hero on land as close as possible
if (!regicideGarrison && TriggerHelper.EntityMatchesClassList(spawnPoints[0], "Ship"))
{
let shipPosition = Engine.QueryInterface(spawnPoints[0], IID_Position).GetPosition2D();
let distanceToShip = entity =>
Engine.QueryInterface(entity, IID_Position).GetPosition2D().distanceToSquared(shipPosition);
spawnPoints = TriggerHelper.GetLandSpawnPoints().sort((entity1, entity2) =>
distanceToShip(entity1) - distanceToShip(entity2));
}
this.regicideHeroes[playerID] = this.SpawnRegicideHero(playerID, heroTemplates[playersCivs[playerID]], spawnPoints);
}
};
/**
* Spawn a random hero at one of the given locations (which are checked in order).
* Garrison it if the location is a ship.
*
* @param spawnPoints - entity IDs at which to spawn
*/
Trigger.prototype.SpawnRegicideHero = function(playerID, heroTemplates, spawnPoints)
{
for (let heroTemplate of shuffleArray(heroTemplates))
for (let spawnPoint of spawnPoints)
{
let cmpPosition = Engine.QueryInterface(spawnPoint, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
// Consider nomad maps where units start on a ship
let isShip = TriggerHelper.EntityMatchesClassList(spawnPoint, "Ship");
if (isShip)
{
let cmpGarrisonHolder = Engine.QueryInterface(spawnPoint, IID_GarrisonHolder);
if (cmpGarrisonHolder.IsFull() ||
!MatchesClassList(heroTemplate.classes, cmpGarrisonHolder.GetAllowedClasses()))
continue;
}
let hero = TriggerHelper.SpawnUnits(spawnPoint, heroTemplate.templateName, 1, playerID);
if (!hero.length)
continue;
hero = hero[0];
if (isShip)
{
let cmpUnitAI = Engine.QueryInterface(hero, IID_UnitAI);
cmpUnitAI.Garrison(spawnPoint);
}
return hero;
}
error("Couldn't spawn hero for player " + playerID);
return undefined;
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.regicideHeroes = [];
cmpTrigger.DoAfterDelay(0, "InitRegicideGame", {});
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckRegicideDefeat", { "enabled": true });
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js (revision 21474)
@@ -1,153 +1,153 @@
Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
{
let cmpWonder = Engine.QueryInterface(data.entity, IID_Wonder);
if (!cmpWonder)
return;
this.WonderVictoryDeleteTimer(data.entity);
if (data.to > 0)
this.WonderVictoryStartTimer(data.entity, data.to);
};
Trigger.prototype.WonderVictoryDiplomacyChanged = function(data)
{
if (!Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager).GetAlliedVictory())
return;
for (let ent in this.wonderVictoryMessages)
{
if (this.wonderVictoryMessages[ent].playerID != data.player && this.wonderVictoryMessages[ent].playerID != data.otherPlayer)
continue;
let owner = this.wonderVictoryMessages[ent].playerID;
let otherPlayer = owner == data.player ? data.otherPlayer : data.player;
let newAllies = new Set(QueryPlayerIDInterface(owner).GetPlayersByDiplomacy("IsExclusiveMutualAlly"));
if (newAllies.has(otherPlayer) && !this.wonderVictoryMessages[ent].allies.has(otherPlayer) ||
!newAllies.has(otherPlayer) && this.wonderVictoryMessages[ent].allies.has(otherPlayer))
{
this.WonderVictoryDeleteTimer(ent);
this.WonderVictoryStartTimer(ent, owner);
}
}
};
/**
* Create new messages, and start timer to register defeat.
*/
Trigger.prototype.WonderVictoryStartTimer = function(ent, player)
{
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
let allies = cmpEndGameManager.GetAlliedVictory() ?
QueryPlayerIDInterface(player).GetPlayersByDiplomacy("IsExclusiveMutualAlly") : [];
let others = [-1];
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
{
let cmpPlayer = QueryPlayerIDInterface(playerID);
if (cmpPlayer.GetState() == "won")
return;
if (allies.indexOf(playerID) == -1 && playerID != player)
others.push(playerID);
}
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- let wonderDuration = cmpEndGameManager.GetGameTypeSettings().wonderDuration;
+ let wonderDuration = cmpEndGameManager.GetGameSettings().wonderDuration;
this.wonderVictoryMessages[ent] = {
"playerID": player,
"allies": new Set(allies),
"timer": cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "WonderVictorySetWinner", wonderDuration, player),
"messages": [
cmpGuiInterface.AddTimeNotification(
{
"message": allies.length ?
markForTranslation("%(_player_)s owns a Wonder and %(_player_)s and their allies will win in %(time)s.") :
markForTranslation("%(_player_)s owns a Wonder and will win in %(time)s."),
"players": others,
"parameters": {
"_player_": player
},
"translateMessage": true,
"translateParameters": []
},
wonderDuration),
cmpGuiInterface.AddTimeNotification(
{
"message": markForTranslation("%(_player_)s owns a Wonder and you will win in %(time)s."),
"players": allies,
"parameters": {
"_player_": player
},
"translateMessage": true,
"translateParameters": []
},
wonderDuration),
cmpGuiInterface.AddTimeNotification(
{
"message": allies.length ?
markForTranslation("You own a Wonder and you and your allies will win in %(time)s.") :
markForTranslation("You own a Wonder and will win in %(time)s."),
"players": [player],
"translateMessage": true
},
wonderDuration)
]
};
};
Trigger.prototype.WonderVictoryDeleteTimer = function(ent)
{
if (!this.wonderVictoryMessages[ent])
return;
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
for (let message of this.wonderVictoryMessages[ent].messages)
cmpGuiInterface.DeleteTimeNotification(message);
cmpTimer.CancelTimer(this.wonderVictoryMessages[ent].timer);
delete this.wonderVictoryMessages[ent];
};
Trigger.prototype.WonderVictoryPlayerWon = function(data)
{
for (let ent in this.wonderVictoryMessages)
this.WonderVictoryDeleteTimer(ent);
};
Trigger.prototype.WonderVictoryPlayerDefeated = function(data)
{
for (let ent in this.wonderVictoryMessages)
if (this.wonderVictoryMessages[ent].allies.has(data.playerId))
{
let owner = this.wonderVictoryMessages[ent].playerID;
this.WonderVictoryDeleteTimer(ent);
this.WonderVictoryStartTimer(ent, owner);
}
};
Trigger.prototype.WonderVictorySetWinner = function(playerID)
{
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
cmpEndGameManager.MarkPlayerAndAlliesAsWon(
playerID,
n => markForPluralTranslation(
"%(lastPlayer)s has won (wonder victory).",
"%(players)s and %(lastPlayer)s have won (wonder victory).",
n),
n => markForPluralTranslation(
"%(lastPlayer)s has been defeated (wonder victory).",
"%(players)s and %(lastPlayer)s have been defeated (wonder victory).",
n));
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "WonderVictoryOwnershipChanged", { "enabled": true });
cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "WonderVictoryDiplomacyChanged", { "enabled": true });
cmpTrigger.RegisterTrigger("OnPlayerWon", "WonderVictoryPlayerWon", { "enabled": true });
cmpTrigger.RegisterTrigger("OnPlayerDefeated", "WonderVictoryPlayerDefeated", { "enabled": true });
cmpTrigger.wonderVictoryMessages = {};
}
Index: ps/trunk/binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.xml (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.xml (revision 21474)
@@ -1,4876 +1,4876 @@
default
0.00136719
0.205078
lake
19.2
4.0
0.916016
0
0.995117
0.99
0.2
default
structures/spart_civil_centre
1
structures/athen_civil_centre
2
units/spart_support_female_citizen
1
units/spart_support_female_citizen
1
units/spart_support_female_citizen
1
actor|props/flora/bush.xml
actor|props/flora/bush.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la_lush.xml
actor|props/flora/bush_medit_la_lush.xml
actor|props/flora/bush_medit_la_lush.xml
actor|props/flora/bush_medit_la_lush.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_underbrush.xml
actor|props/flora/bush_medit_underbrush.xml
actor|props/flora/bush_medit_underbrush.xml
actor|props/flora/bush_tempe_a.xml
actor|props/flora/bush_tempe_a.xml
actor|props/flora/bush_tempe_a.xml
actor|props/flora/bush_tempe_a.xml
actor|props/flora/bush_tempe_a.xml
actor|props/flora/bush_tempe_b.xml
actor|props/flora/bush_tempe_b.xml
actor|props/flora/bush_tempe_b.xml
actor|props/flora/bush_tempe_b.xml
actor|props/flora/bush_tempe_b.xml
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_oak
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me_lush.xml
actor|props/flora/bush_medit_me_lush.xml
actor|props/flora/bush_medit_me_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_tempe_me_dry.xml
actor|props/flora/bush_tempe_me_lush.xml
actor|props/flora/bush_tempe_sm_lush.xml
actor|props/flora/bush_tempe_sm_lush.xml
actor|props/flora/bush_tempe_sm_lush.xml
actor|props/flora/bush_tempe_sm_lush.xml
actor|props/flora/bush_tempe_sm_lush.xml
actor|props/flora/bush_tempe_sm_lush.xml
structures/spart_gerousia
1
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_tempe_la.xml
actor|props/flora/bush_tempe_la.xml
actor|props/flora/bush_tempe_la.xml
actor|props/flora/bush_tempe_la.xml
actor|props/flora/bush_medit_la_lush.xml
actor|props/flora/bush_medit_la_lush.xml
actor|props/flora/bush_medit_la_lush.xml
actor|props/flora/bush_medit_la_lush.xml
actor|props/flora/bush_medit_la_lush.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_highlands.xml
actor|props/flora/bush_highlands.xml
actor|props/flora/bush_highlands.xml
actor|props/flora/bush_highlands.xml
actor|props/flora/bush_highlands.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_tempe_sm.xml
actor|props/flora/bush_tempe_sm.xml
actor|props/flora/bush_tempe_sm.xml
actor|props/flora/bush_tempe_sm.xml
actor|props/flora/bush_tempe_sm.xml
actor|props/flora/bush_tempe_sm_lush.xml
actor|props/flora/bush_tempe_sm_lush.xml
actor|props/flora/bush_tempe_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_sm_lush.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me.xml
actor|props/flora/bush_medit_me_lush.xml
actor|props/flora/bush_medit_me_lush.xml
actor|props/flora/bush_tempe_me_lush.xml
actor|props/flora/bush_tempe_me_lush.xml
actor|props/flora/bush_tempe_la_lush.xml
actor|props/flora/bush_tempe_la_lush.xml
actor|props/flora/bush_tempe_la_lush.xml
actor|props/flora/bush_tempe_la_lush.xml
actor|props/flora/bush_tempe_la_lush.xml
actor|props/flora/bush_tempe_la_lush.xml
actor|props/flora/bush_tempe_la_lush.xml
actor|props/flora/foliagebush.xml
actor|props/flora/foliagebush.xml
actor|props/flora/foliagebush.xml
actor|props/flora/foliagebush.xml
actor|props/flora/foliagebush.xml
actor|props/flora/foliagebush.xml
actor|props/flora/bush_tempe_la.xml
actor|props/flora/bush_tempe_la.xml
actor|props/flora/bush_tempe_la.xml
actor|props/flora/bush_tempe_la.xml
actor|props/flora/bush_tempe_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_la.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_medit_sm.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_tempe_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_dead
0
gaia/flora_tree_aleppo_pine
0
gaia/flora_tree_aleppo_pine
0
gaia/flora_tree_aleppo_pine
0
gaia/flora_tree_oak
0
gaia/flora_tree_pine
0
gaia/flora_tree_pine
0
gaia/flora_tree_pine
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_aleppo_pine
0
gaia/flora_tree_aleppo_pine
0
gaia/flora_tree_aleppo_pine
0
gaia/flora_tree_aleppo_pine
0
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/aleppo_pine.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech_top.xml
actor|flora/trees/european_beech_top.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/lumbardypoplar.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/tree_dead.xml
actor|flora/trees/tree_dead.xml
actor|flora/trees/tree_dead.xml
actor|flora/trees/tree_dead.xml
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/flora_bush_temperate
0
gaia/geology_stonemine_medit_quarry
0
gaia/geology_stonemine_medit_quarry
0
actor|props/units/weapons/rock.xml
actor|geology/stone_medit_med.xml
actor|geology/stone_granite_greek_med.xml
actor|geology/stone_granite_greek_large.xml
actor|geology/stone_granite_greek_large.xml
actor|geology/stone_granite_greek_large.xml
actor|geology/stone_granite_greek_small.xml
actor|geology/stone_granite_greek_small.xml
actor|geology/stone_granite_greek_small.xml
actor|props/special/eyecandy/blocks_limestone_pile_b.xml
actor|props/special/eyecandy/blocks_limestone_pile_b.xml
actor|props/special/eyecandy/blocks_limestone_pile_b.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_la_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_me_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
actor|props/flora/bush_medit_sm_dry.xml
gaia/flora_bush_berry
0
gaia/flora_bush_berry
0
gaia/flora_bush_berry
0
gaia/flora_bush_berry
0
gaia/flora_bush_berry
0
gaia/flora_bush_berry
0
gaia/flora_bush_berry
0
gaia/flora_bush_berry
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_poplar
0
gaia/flora_tree_carob
0
other/bridge_hele
0
other/bridge_hele
0
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/oak_old.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/poplar.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/oak_large.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/carob.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech.xml
actor|flora/trees/european_beech_aut.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
actor|flora/trees/pine.xml
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_pine
0
gaia/flora_tree_pine
0
gaia/flora_tree_pine
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_pine
0
gaia/flora_tree_pine
0
gaia/flora_tree_pine
0
structures/athen_temple
2
structures/athen_house
2
structures/athen_house
2
structures/athen_house
2
structures/athen_barracks
2
gaia/flora_tree_dead
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_oak_large
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_carob
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_poplar
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
gaia/flora_tree_euro_beech
0
structures/athen_farmstead
2
structures/athen_field
2
structures/athen_field
2
structures/athen_field
2
structures/athen_field
2
structures/athen_field
2
structures/athen_field
2
structures/athen_field
2
structures/athen_outpost
2
structures/athen_outpost
2
gaia/geology_stonemine_medit_quarry
0
gaia/geology_stonemine_medit_quarry
0
gaia/geology_metal_mediterranean_slabs
0
gaia/geology_metal_mediterranean_slabs
0
structures/athen_storehouse
2
units/spart_infantry_javelinist_b
1
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_support_female_citizen
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_ranged
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_marine
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_champion_infantry
2
units/athen_infantry_spearman_b
2
units/athen_infantry_spearman_b
2
units/athen_infantry_spearman_b
2
units/athen_infantry_spearman_b
2
units/athen_infantry_spearman_b
2
units/athen_infantry_spearman_b
2
units/athen_infantry_javelinist_b
2
units/athen_infantry_javelinist_b
2
units/athen_infantry_javelinist_b
2
units/athen_infantry_javelinist_b
2
gaia/flora_bush_berry
0
gaia/flora_bush_berry
0
gaia/flora_bush_berry
0
Index: ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.xml (revision 21473)
+++ ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.xml (revision 21474)
@@ -1,8704 +1,8704 @@
default
0
0.5
lake
19.9
4.0
0.45
0
1
0.99
0.1999
default
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/geology_metal_desert_slabs
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_dry_a.xml
gaia/geology_stonemine_desert_badlands_quarry
0
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_dry_a.xml
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/geology_metal_desert_slabs
0
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_stonemine_desert_badlands_quarry
0
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/plant_desert_a.xml
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
structures/athen_civil_centre
1
units/athen_support_female_citizen
1
units/athen_support_female_citizen
1
units/athen_support_female_citizen
1
units/athen_support_female_citizen
1
units/athen_infantry_spearman_b
1
units/athen_infantry_spearman_b
1
units/athen_infantry_spearman_b
1
units/athen_infantry_spearman_b
1
units/athen_cavalry_javelinist_b
1
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/geology_metal_desert_slabs
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
gaia/geology_stonemine_desert_badlands_quarry
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_dry_a.xml
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/fauna_chicken
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/flora_bush_grapes
0
gaia/geology_metal_desert_slabs
0
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_stonemine_desert_badlands_quarry
0
actor|props/flora/plant_desert_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/plant_desert_a.xml
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/geology_stonemine_desert_badlands_quarry
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
gaia/geology_stonemine_desert_badlands_quarry
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/plant_desert_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_metal_desert_slabs
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
gaia/geology_metal_desert_slabs
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stonemine_desert_badlands_quarry
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_stonemine_desert_badlands_quarry
0
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_dry_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/plant_desert_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/plant_desert_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/plant_desert_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
gaia/geology_stone_desert_small
0
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
gaia/geology_metal_desert_slabs
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
gaia/geology_metal_desert_slabs
0
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
gaia/geology_metal_desert_slabs
0
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_dry_a.xml
actor|props/flora/bush_dry_a.xml
gaia/geology_metal_desert_slabs
0
actor|props/flora/plant_desert_a.xml
actor|props/flora/bush_desert_a.xml
gaia/geology_metal_desert_slabs
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/plant_desert_a.xml
gaia/geology_metal_desert_slabs
0
actor|props/flora/bush_dry_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/plant_desert_a.xml
actor|props/flora/plant_desert_a.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|geology/stone_desert_med.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_dry_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
actor|props/flora/bush_desert_a.xml
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_goat
0
gaia/fauna_sheep
0
gaia/fauna_sheep
0
gaia/fauna_sheep
0
gaia/fauna_sheep
0
gaia/fauna_sheep
0
gaia/fauna_sheep
0
gaia/fauna_sheep
0
gaia/fauna_sheep
0
gaia/fauna_sheep
0
gaia/fauna_sheep
0
gaia/fauna_camel
0
gaia/fauna_camel
0
gaia/fauna_camel
0
gaia/fauna_camel
0
gaia/fauna_camel
0
gaia/fauna_camel
0
gaia/fauna_camel
0
gaia/fauna_camel
0
gaia/fauna_camel
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_tamarix
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
gaia/flora_tree_oak
0
Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js (revision 21474)
@@ -1,395 +1,395 @@
var API3 = function(m)
{
/** Shared script handling templates and basic terrain analysis */
m.SharedScript = function(settings)
{
if (!settings)
return;
this._players = Object.keys(settings.players).map(key => settings.players[key]); // TODO SM55 Object.values(settings.players)
this._templates = settings.templates;
this._entityMetadata = {};
for (let player of this._players)
this._entityMetadata[player] = {};
// array of entity collections
this._entityCollections = new Map();
this._entitiesModifications = new Map(); // entities modifications
this._templatesModifications = {}; // template modifications
// each name is a reference to the actual one.
this._entityCollectionsName = new Map();
this._entityCollectionsByDynProp = {};
this._entityCollectionsUID = 0;
};
/** Return a simple object (using no classes etc) that will be serialized into saved games */
m.SharedScript.prototype.Serialize = function()
{
return {
"players": this._players,
"templatesModifications": this._templatesModifications,
"entitiesModifications": this._entitiesModifications,
"metadata": this._entityMetadata
};
};
/**
* Called after the constructor when loading a saved game, with 'data' being
* whatever Serialize() returned
*/
m.SharedScript.prototype.Deserialize = function(data)
{
this._players = data.players;
this._templatesModifications = data.templatesModifications;
this._entitiesModifications = data.entitiesModifications;
this._entityMetadata = data.metadata;
this.isDeserialized = true;
};
m.SharedScript.prototype.GetTemplate = function(name)
{
if (this._templates[name] === undefined)
this._templates[name] = Engine.GetTemplate(name) || null;
return this._templates[name];
};
/**
* Initialize the shared component.
* We need to know the initial state of the game for this, as we will use it.
* This is called right at the end of the map generation.
*/
m.SharedScript.prototype.init = function(state, deserialization)
{
if (!deserialization)
this._entitiesModifications = new Map();
this.ApplyTemplatesDelta(state);
this.passabilityClasses = state.passabilityClasses;
this.playersData = state.players;
this.timeElapsed = state.timeElapsed;
this.circularMap = state.circularMap;
this.mapSize = state.mapSize;
- this.victoryConditions = new Set([state.gameType]);
+ this.victoryConditions = new Set(state.victoryConditions);
this.alliedVictory = state.alliedVictory;
this.ceasefireActive = state.ceasefireActive;
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
this.passabilityMap = state.passabilityMap;
if (this.mapSize % this.passabilityMap.width !== 0)
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while passability=" + this.passabilityMap.width);
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
this.territoryMap = state.territoryMap;
if (this.mapSize % this.territoryMap.width !== 0)
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while territory=" + this.territoryMap.width);
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
/*
let landPassMap = new Uint8Array(this.passabilityMap.data.length);
let waterPassMap = new Uint8Array(this.passabilityMap.data.length);
let obstructionMaskLand = this.passabilityClasses["default-terrain-only"];
let obstructionMaskWater = this.passabilityClasses["ship-terrain-only"];
for (let i = 0; i < this.passabilityMap.data.length; ++i)
{
landPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
waterPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255;
}
Engine.DumpImage("LandPassMap.png", landPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
Engine.DumpImage("WaterPassMap.png", waterPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
*/
this._entities = new Map();
if (state.entities)
for (let id in state.entities)
this._entities.set(+id, new m.Entity(this, state.entities[id]));
// entity collection updated on create/destroy event.
this.entities = new m.EntityCollection(this, this._entities);
// create the terrain analyzer
this.terrainAnalyzer = new m.TerrainAnalysis();
this.terrainAnalyzer.init(this, state);
this.accessibility = new m.Accessibility();
this.accessibility.init(state, this.terrainAnalyzer);
// Resource types: ignore = not used for resource maps
// abundant = abundant resource with small amount each
// sparse = sparse resource, but huge amount each
// The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites)
// They are updated by checking for create and destroy events for all resources
this.normalizationFactor = { "abundant": 50, "sparse": 90 };
this.influenceRadius = { "abundant": 36, "sparse": 48 };
this.ccInfluenceRadius = { "abundant": 60, "sparse": 120 };
this.resourceMaps = {}; // Contains maps showing the density of resources
this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement.
this.createResourceMaps();
this.gameState = {};
for (let player of this._players)
{
this.gameState[player] = new m.GameState();
this.gameState[player].init(this, state, player);
}
};
/**
* General update of the shared script, before each AI's update
* applies entity deltas, and each gamestate.
*/
m.SharedScript.prototype.onUpdate = function(state)
{
if (this.isDeserialized)
{
this.init(state, true);
this.isDeserialized = false;
}
// deals with updating based on create and destroy messages.
this.ApplyEntitiesDelta(state);
this.ApplyTemplatesDelta(state);
Engine.ProfileStart("onUpdate");
// those are dynamic and need to be reset as the "state" object moves in memory.
this.events = state.events;
this.passabilityClasses = state.passabilityClasses;
this.playersData = state.players;
this.timeElapsed = state.timeElapsed;
this.barterPrices = state.barterPrices;
this.ceasefireActive = state.ceasefireActive;
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
this.passabilityMap = state.passabilityMap;
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
this.territoryMap = state.territoryMap;
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
for (let i in this.gameState)
this.gameState[i].update(this);
// TODO: merge this with "ApplyEntitiesDelta" since after all they do the same.
this.updateResourceMaps(this.events);
Engine.ProfileStop();
};
m.SharedScript.prototype.ApplyEntitiesDelta = function(state)
{
Engine.ProfileStart("Shared ApplyEntitiesDelta");
let foundationFinished = {};
// by order of updating:
// we "Destroy" last because we want to be able to switch Metadata first.
for (let evt of state.events.Create)
{
if (!state.entities[evt.entity])
continue; // Sometimes there are things like foundations which get destroyed too fast
let entity = new m.Entity(this, state.entities[evt.entity]);
this._entities.set(evt.entity, entity);
this.entities.addEnt(entity);
// Update all the entity collections since the create operation affects static properties as well as dynamic
for (let entCol of this._entityCollections.values())
entCol.updateEnt(entity);
}
for (let evt of state.events.EntityRenamed)
{ // Switch the metadata: TODO entityCollections are updated only because of the owner change. Should be done properly
for (let player of this._players)
{
this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity];
this._entityMetadata[player][evt.entity] = {};
}
}
for (let evt of state.events.TrainingFinished)
{ // Apply metadata stored in training queues
for (let entId of evt.entities)
if (this._entities.has(entId))
for (let key in evt.metadata)
this.setMetadata(evt.owner, this._entities.get(entId), key, evt.metadata[key]);
}
for (let evt of state.events.ConstructionFinished)
{
// metada are already moved by EntityRenamed when needed (i.e. construction, not repair)
if (evt.entity != evt.newentity)
foundationFinished[evt.entity] = true;
}
for (let evt of state.events.AIMetadata)
{
if (!this._entities.has(evt.id))
continue; // might happen in some rare cases of foundations getting destroyed, perhaps.
// Apply metadata (here for buildings for example)
for (let key in evt.metadata)
this.setMetadata(evt.owner, this._entities.get(evt.id), key, evt.metadata[key]);
}
for (let evt of state.events.Destroy)
{
if (!this._entities.has(evt.entity))
continue;// probably should remove the event.
if (foundationFinished[evt.entity])
evt.SuccessfulFoundation = true;
// The entity was destroyed but its data may still be useful, so
// remember the entity and this AI's metadata concerning it
evt.metadata = {};
evt.entityObj = this._entities.get(evt.entity);
for (let player of this._players)
evt.metadata[player] = this._entityMetadata[player][evt.entity];
let entity = this._entities.get(evt.entity);
for (let entCol of this._entityCollections.values())
entCol.removeEnt(entity);
this.entities.removeEnt(entity);
this._entities.delete(evt.entity);
this._entitiesModifications.delete(evt.entity);
for (let player of this._players)
delete this._entityMetadata[player][evt.entity];
}
for (let id in state.entities)
{
let changes = state.entities[id];
let entity = this._entities.get(+id);
for (let prop in changes)
{
entity._entity[prop] = changes[prop];
this.updateEntityCollections(prop, entity);
}
}
// apply per-entity aura-related changes.
// this supersedes tech-related changes.
for (let id in state.changedEntityTemplateInfo)
{
if (!this._entities.has(+id))
continue; // dead, presumably.
let changes = state.changedEntityTemplateInfo[id];
if (!this._entitiesModifications.has(+id))
this._entitiesModifications.set(+id, new Map());
let modif = this._entitiesModifications.get(+id);
for (let change of changes)
modif.set(change.variable, change.value);
}
Engine.ProfileStop();
};
m.SharedScript.prototype.ApplyTemplatesDelta = function(state)
{
Engine.ProfileStart("Shared ApplyTemplatesDelta");
for (let player in state.changedTemplateInfo)
{
let playerDiff = state.changedTemplateInfo[player];
for (let template in playerDiff)
{
let changes = playerDiff[template];
if (!this._templatesModifications[template])
this._templatesModifications[template] = {};
if (!this._templatesModifications[template][player])
this._templatesModifications[template][player] = new Map();
let modif = this._templatesModifications[template][player];
for (let change of changes)
modif.set(change.variable, change.value);
}
}
Engine.ProfileStop();
};
m.SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection)
{
entCollection.setUID(this._entityCollectionsUID);
this._entityCollections.set(this._entityCollectionsUID, entCollection);
for (let prop of entCollection.dynamicProperties())
{
if (!this._entityCollectionsByDynProp[prop])
this._entityCollectionsByDynProp[prop] = new Map();
this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection);
}
this._entityCollectionsUID++;
};
m.SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection)
{
let uid = entCollection.getUID();
if (this._entityCollections.has(uid))
this._entityCollections.delete(uid);
for (let prop of entCollection.dynamicProperties())
if (this._entityCollectionsByDynProp[prop].has(uid))
this._entityCollectionsByDynProp[prop].delete(uid);
};
m.SharedScript.prototype.updateEntityCollections = function(property, ent)
{
if (this._entityCollectionsByDynProp[property] === undefined)
return;
for (let entCol of this._entityCollectionsByDynProp[property].values())
entCol.updateEnt(ent);
};
m.SharedScript.prototype.setMetadata = function(player, ent, key, value)
{
let metadata = this._entityMetadata[player][ent.id()];
if (!metadata)
{
this._entityMetadata[player][ent.id()] = {};
metadata = this._entityMetadata[player][ent.id()];
}
metadata[key] = value;
this.updateEntityCollections('metadata', ent);
this.updateEntityCollections('metadata.' + key, ent);
};
m.SharedScript.prototype.getMetadata = function(player, ent, key)
{
let metadata = this._entityMetadata[player][ent.id()];
if (!metadata || !(key in metadata))
return undefined;
return metadata[key];
};
m.SharedScript.prototype.deleteMetadata = function(player, ent, key)
{
let metadata = this._entityMetadata[player][ent.id()];
if (!metadata || !(key in metadata))
return true;
metadata[key] = undefined;
delete metadata[key];
this.updateEntityCollections('metadata', ent);
this.updateEntityCollections('metadata.' + key, ent);
return true;
};
m.copyPrototype = function(descendant, parent)
{
let sConstructor = parent.toString();
let aMatch = sConstructor.match(/\s*function (.*)\(/);
if (aMatch != null)
descendant.prototype[aMatch[1]] = parent;
for (let p in parent.prototype)
descendant.prototype[p] = parent.prototype[p];
};
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js (revision 21474)
@@ -1,202 +1,199 @@
/**
- * System component to store the gametype, gametype settings and
+ * System component to store the victory conditions and their settings and
* check for allied victory / last-man-standing.
*/
function EndGameManager() {}
EndGameManager.prototype.Schema =
"";
EndGameManager.prototype.Init = function()
{
- this.gameType = "conquest";
-
// Contains settings specific to the victory condition,
// for example wonder victory duration.
- this.gameTypeSettings = {};
+ this.gameSettings = {};
// Allied victory means allied players can win if victory conditions are met for each of them
// False for a "last man standing" game
this.alliedVictory = true;
// Don't do any checks before the diplomacies were set for each player
// or when marking a player as won.
this.skipAlliedVictoryCheck = true;
this.lastManStandingMessage = undefined;
this.endlessGame = false;
};
-EndGameManager.prototype.GetGameType = function()
+EndGameManager.prototype.GetGameSettings = function()
{
- return this.gameType;
+ return this.gameSettings;
};
-EndGameManager.prototype.GetGameTypeSettings = function()
+EndGameManager.prototype.GetVictoryConditions = function()
{
- return this.gameTypeSettings;
+ return this.gameSettings.victoryConditions;
};
-EndGameManager.prototype.SetGameType = function(newGameType, newSettings = {})
+EndGameManager.prototype.SetGameSettings = function(newSettings = {})
{
- this.gameType = newGameType;
- this.gameTypeSettings = newSettings;
+ this.gameSettings = newSettings;
this.skipAlliedVictoryCheck = false;
- this.endlessGame = newGameType == "endless";
+ this.endlessGame = !this.gameSettings.victoryConditions.length;
- Engine.BroadcastMessage(MT_GameTypeChanged, {});
+ Engine.BroadcastMessage(MT_VictoryConditionsChanged, {});
};
/**
* Sets the given player (and the allies if allied victory is enabled) as a winner.
*
* @param {number} playerID - The player that should win.
* @param {function} victoryReason - Function that maps from number to plural string, for example
* n => markForPluralTranslation(
* "%(lastPlayer)s has won (game mode).",
* "%(players)s and %(lastPlayer)s have won (game mode).",
* n));
*/
EndGameManager.prototype.MarkPlayerAndAlliesAsWon = function(playerID, victoryString, defeatString)
{
let state = QueryPlayerIDInterface(playerID).GetState();
if (state != "active")
{
warn("Can't mark player " + playerID + " as won, since the state is " + state);
return;
}
let winningPlayers = [playerID];
if (this.alliedVictory)
winningPlayers = QueryPlayerIDInterface(playerID).GetMutualAllies(playerID).filter(
player => QueryPlayerIDInterface(player).GetState() == "active");
this.MarkPlayersAsWon(winningPlayers, victoryString, defeatString);
};
/**
* Sets the given players as won and others as defeated.
*
* @param {array} winningPlayers - The players that should win.
* @param {function} victoryReason - Function that maps from number to plural string, for example
* n => markForPluralTranslation(
* "%(lastPlayer)s has won (game mode).",
* "%(players)s and %(lastPlayer)s have won (game mode).",
* n));
*/
EndGameManager.prototype.MarkPlayersAsWon = function(winningPlayers, victoryString, defeatString)
{
this.skipAlliedVictoryCheck = true;
for (let playerID of winningPlayers)
{
let cmpPlayer = QueryPlayerIDInterface(playerID);
let state = cmpPlayer.GetState();
if (state != "active")
{
warn("Can't mark player " + playerID + " as won, since the state is " + state);
continue;
}
cmpPlayer.SetState("won", undefined);
}
let defeatedPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNonGaiaPlayers().filter(
playerID => winningPlayers.indexOf(playerID) == -1 && QueryPlayerIDInterface(playerID).GetState() == "active");
for (let playerID of defeatedPlayers)
QueryPlayerIDInterface(playerID).SetState("defeated", undefined);
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "won",
"players": [winningPlayers[0]],
"allies" : winningPlayers,
"message": victoryString(winningPlayers.length)
});
if (defeatedPlayers.length)
cmpGUIInterface.PushNotification({
"type": "defeat",
"players": [defeatedPlayers[0]],
"allies" : defeatedPlayers,
"message": defeatString(defeatedPlayers.length)
});
this.skipAlliedVictoryCheck = false;
};
EndGameManager.prototype.SetAlliedVictory = function(flag)
{
this.alliedVictory = flag;
};
EndGameManager.prototype.GetAlliedVictory = function()
{
return this.alliedVictory;
};
EndGameManager.prototype.AlliedVictoryCheck = function()
{
if (this.skipAlliedVictoryCheck || this.endlessGame)
return;
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.DeleteTimeNotification(this.lastManStandingMessage);
// Proceed if only allies are remaining
let allies = [];
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let playerID = 1; playerID < numPlayers; ++playerID)
{
let cmpPlayer = QueryPlayerIDInterface(playerID);
if (cmpPlayer.GetState() != "active")
continue;
if (allies.length && !cmpPlayer.IsMutualAlly(allies[0]))
return;
allies.push(playerID);
}
if (this.alliedVictory || allies.length == 1)
{
for (let playerID of allies)
{
let cmpPlayer = QueryPlayerIDInterface(playerID);
if (cmpPlayer)
cmpPlayer.SetState("won", undefined);
}
cmpGuiInterface.PushNotification({
"type": "won",
"players": [allies[0]],
"allies" : allies,
"message": markForPluralTranslation(
"%(lastPlayer)s has won (last player alive).",
"%(players)s and %(lastPlayer)s have won (last players alive).",
allies.length)
});
}
else
this.lastManStandingMessage = cmpGuiInterface.AddTimeNotification({
"message": markForTranslation("Last remaining player wins."),
"translateMessage": true,
}, 12 * 60 * 60 * 1000); // 12 hours
};
EndGameManager.prototype.OnInitGame = function(msg)
{
this.AlliedVictoryCheck();
};
EndGameManager.prototype.OnGlobalDiplomacyChanged = function(msg)
{
this.AlliedVictoryCheck();
};
EndGameManager.prototype.OnGlobalPlayerDefeated = function(msg)
{
this.AlliedVictoryCheck();
};
Engine.RegisterSystemComponentType(IID_EndGameManager, "EndGameManager", EndGameManager);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 21474)
@@ -1,1975 +1,1975 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised for the biggest part
// So most of the attributes shouldn't be serialized
// Return an object with a small selection of deterministic data
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out what phase we are in
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
// store player ally/neutral/enemy data as arrays
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
// Add timeElapsed
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
// Add ceasefire info
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
// Add cinema path info
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
// Add the game type and allied victory
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
- ret.gameType = cmpEndGameManager.GetGameType();
+ ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
// Add basic statistics to each player
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
// Get basic simulation info
let ret = this.GetSimulationState();
// Add statistics to each player
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
ret.position = cmpPosition.GetPosition();
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval"),
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress(),
};
var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades" : cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo()
};
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"possibleStances": cmpUnitAI.GetPossibleStances(),
"isIdle":cmpUnitAI.IsIdle(),
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities(),
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked(),
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = cmpAttack.GetAttackStrengths(type);
ret.attack[type].splash = cmpAttack.GetSplashDamage(type);
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// not a ranged attack, set some defaults
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the range in front of it, no spread. So angle = 0
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
}
else
{
// not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
}
let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
ret.armour = cmpArmour.GetArmourStrengths();
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"hp": cmpHeal.GetHP(),
"range": cmpHeal.GetRange().max,
"rate": cmpHeal.GetRate(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetTimer(),
"rates": cmpResourceTrickle.GetRates()
};
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetRunSpeed()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, templateName)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
aurasTemplate[name] = AuraTemplates.Get(name);
return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
// Checks whether the requirements for this technology have been met
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
// Returns technologies that are being actively researched, along with
// which entity is researching them and how far along the research is.
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
}
}
return ret;
};
// Returns the battle state of the player.
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
// Returns a list of ongoing attacks against the player.
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
};
// Used to show a red square over GUI elements you can't yet afford.
GuiInterface.prototype.GetNeededResources = function(player, data)
{
return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost);
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// filter on players and time, since the delete timer might be executed with a delay
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
return QueryPlayerIDInterface(wantedPlayer).GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
let playerColors = {}; // cache of owner -> color map
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color:
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r":1, "g":1, "b":1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location)
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
if (pos)
{
// Only update the position if we changed it (cmd.queued is set)
if ("queued" in cmd)
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
else
cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": [],
};
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
// Move the preview into the right location
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* 'populationBonus': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
// --------------------------------------------------------------------------------
// do some entity cache management and check for snapping
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// we're clearing the preview, clear the entity cache and bail
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// keep template data around
}
return false;
}
// Move all existing cached entities outside of the world and reset their use count
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before
for (let type in wallSet.templates)
{
if (type == "curves")
continue;
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, tpl),
};
// ensure that the loaded template data contains a wallPiece component
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// prevent division by zero errors further on if the start and end positions are the same
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// clear the single-building preview entity (we'll be rolling our own)
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// calculate wall placement and position preview entities
let result = {
"pieces": [],
"cost": { "population": 0, "populationBonus": 0, "time": 0 },
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length > 0 && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true, // preview only, must not appear in the result
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length-1].controlGroups = previewEntities[previewEntities.length-1].controlGroups || [];
previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
// allocate new entity
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
// reuse an existing one
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// move piece to right location
// TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
// check whether this wall piece can be validly positioned here
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region
// TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest
// (TODO: break unlikely ties by choosing the lowest entity ID)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
var bucket = filtered.bucket;
if(bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if(!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
{
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
}
else if (!firstMarket)
{
result = { "type": "set first" };
}
else if (!secondMarket)
{
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
}
else
{
// Else both markets are not null and target is different from them
result = { "type": "set first" };
}
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
return QueryPlayerIDInterface(player).GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
// trusted and indicates the player associated with the current client; no data should
// be returned unless this player is meant to be able to see it.)
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
throw new Error("Invalid GuiInterface Call name \""+name+"\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/EndGameManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/EndGameManager.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/EndGameManager.js (revision 21474)
@@ -1,7 +1,7 @@
Engine.RegisterInterface("EndGameManager");
/**
* Message of the form {}
* sent from EndGameManager component.
*/
-Engine.RegisterMessageType("GameTypeChanged");
+Engine.RegisterMessageType("VictoryConditionsChanged");
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EndGameManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EndGameManager.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EndGameManager.js (revision 21474)
@@ -1,29 +1,32 @@
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("EndGameManager.js");
let cmpEndGameManager = ConstructComponent(SYSTEM_ENTITY, "EndGameManager");
let playerEnt1 = 1;
let wonderDuration = 2 * 60 * 1000;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetNumPlayers": () => 4
});
AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
"DeleteTimeNotification": () => null,
"AddTimeNotification": () => 1
});
AddMock(playerEnt1, IID_Player, {
"GetName": () => "Player 1",
"GetState": () => "active",
});
TS_ASSERT_EQUALS(cmpEndGameManager.skipAlliedVictoryCheck, true);
cmpEndGameManager.SetAlliedVictory(true);
TS_ASSERT_EQUALS(cmpEndGameManager.GetAlliedVictory(), true);
-cmpEndGameManager.SetGameType("wonder", { "wonderDuration": wonderDuration });
+cmpEndGameManager.SetGameSettings({
+ "victoryConditions": ["wonder"],
+ "wonderDuration": wonderDuration
+});
TS_ASSERT_EQUALS(cmpEndGameManager.skipAlliedVictoryCheck, false);
-TS_ASSERT(cmpEndGameManager.GetGameType() == "wonder");
-TS_ASSERT_EQUALS(cmpEndGameManager.GetGameTypeSettings().wonderDuration, wonderDuration);
+TS_ASSERT_UNEVAL_EQUALS(cmpEndGameManager.GetVictoryConditions(), ["wonder"]);
+TS_ASSERT_EQUALS(cmpEndGameManager.GetGameSettings().wonderDuration, wonderDuration);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 21474)
@@ -1,590 +1,590 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetNames": () => ({
"food": "Food",
"metal": "Metal",
"stone": "Stone",
"wood": "Wood"
}),
"GetResource": resource => ({
"aiAnalysisInfluenceGroup":
resource == "food" ? "ignore" :
resource == "wood" ? "abundant" : "sparse"
})
};
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_Barter, {
GetPrices: function() {
return {
"buy": { "food": 150 },
"sell": { "food": 25 }
};
},
PlayerHasMarket: function () { return false; }
});
AddMock(SYSTEM_ENTITY, IID_EndGameManager, {
- GetGameType: function() { return "conquest"; },
+ GetVictoryConditions: () => ["conquest", "wonder"],
GetAlliedVictory: function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
GetNumPlayers: function() { return 2; },
GetPlayerByID: function(id) { TS_ASSERT(id === 0 || id === 1); return 100+id; }
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
GetLosVisibility: function(ent, player) { return "visible"; },
GetLosCircular: function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
GetCurrentTemplateName: function(ent) { return "example"; },
GetTemplate: function(name) { return ""; }
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
GetTime: function() { return 0; },
SetTimeout: function(ent, iid, funcname, time, data) { return 0; }
});
AddMock(100, IID_Player, {
GetName: function() { return "Player 1"; },
GetCiv: function() { return "gaia"; },
GetColor: function() { return { r: 1, g: 1, b: 1, a: 1}; },
CanControlAllUnits: function() { return false; },
GetPopulationCount: function() { return 10; },
GetPopulationLimit: function() { return 20; },
GetMaxPopulation: function() { return 200; },
GetResourceCounts: function() { return { food: 100 }; },
GetPanelEntities: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() { return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
IsAlly: function() { return false; },
IsMutualAlly: function() { return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return true; },
GetDisabledTemplates: function() { return {}; },
GetDisabledTechnologies: function() { return {}; },
GetSpyCostMultiplier: function() { return 1; },
HasSharedDropsites: function() { return false; },
HasSharedLos: function() { return false; }
});
AddMock(100, IID_EntityLimits, {
GetLimits: function() { return {"Foo": 10}; },
GetCounts: function() { return {"Foo": 5}; },
GetLimitChangers: function() {return {"Foo": {}}; }
});
AddMock(100, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(100, IID_StatisticsTracker, {
GetBasicStatistics: function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
GetSequences: function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; }
});
AddMock(101, IID_Player, {
GetName: function() { return "Player 2"; },
GetCiv: function() { return "mace"; },
GetColor: function() { return { r: 1, g: 0, b: 0, a: 1}; },
CanControlAllUnits: function() { return true; },
GetPopulationCount: function() { return 40; },
GetPopulationLimit: function() { return 30; },
GetMaxPopulation: function() { return 300; },
GetResourceCounts: function() { return { food: 200 }; },
GetPanelEntities: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() {return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
IsAlly: function() { return true; },
IsMutualAlly: function() {return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return false; },
GetDisabledTemplates: function() { return {}; },
GetDisabledTechnologies: function() { return {}; },
GetSpyCostMultiplier: function() { return 1; },
HasSharedDropsites: function() { return false; },
HasSharedLos: function() { return false; }
});
AddMock(101, IID_EntityLimits, {
GetLimits: function() { return {"Bar": 20}; },
GetCounts: function() { return {"Bar": 0}; },
GetLimitChangers: function() {return {"Bar": {}}; }
});
AddMock(101, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(101, IID_StatisticsTracker, {
GetBasicStatistics: function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
GetSequences: function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; }
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
players: [
{
name: "Player 1",
civ: "gaia",
color: { r:1, g:1, b:1, a:1 },
controlsAll: false,
popCount: 10,
popLimit: 20,
popMax: 200,
panelEntities: [],
resourceCounts: { food: 100 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
disabledTemplates: {},
disabledTechnologies: {},
hasSharedDropsites: false,
hasSharedLos: false,
spyCostMultiplier: 1,
phase: "village",
isAlly: [false, false],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [true, true],
entityLimits: {"Foo": 10},
entityCounts: {"Foo": 5},
entityLimitChangers: {"Foo": {}},
researchQueued: new Map(),
researchStarted: new Set(),
researchedTechs: new Set(),
classCounts: {},
typeCountsByClass: {},
canBarter: false,
barterPrices: {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
statistics: {
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0
},
percentMapExplored: 10
}
},
{
name: "Player 2",
civ: "mace",
color: { r:1, g:0, b:0, a:1 },
controlsAll: true,
popCount: 40,
popLimit: 30,
popMax: 300,
panelEntities: [],
resourceCounts: { food: 200 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
disabledTemplates: {},
disabledTechnologies: {},
hasSharedDropsites: false,
hasSharedLos: false,
spyCostMultiplier: 1,
phase: "village",
isAlly: [true, true],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [false, false],
entityLimits: {"Bar": 20},
entityCounts: {"Bar": 0},
entityLimitChangers: {"Bar": {}},
researchQueued: new Map(),
researchStarted: new Set(),
researchedTechs: new Set(),
classCounts: {},
typeCountsByClass: {},
canBarter: false,
barterPrices: {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
statistics: {
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0
},
percentMapExplored: 10
}
}
],
circularMap: false,
timeElapsed: 0,
- gameType: "conquest",
+ "victoryConditions": ["conquest", "wonder"],
alliedVictory: false
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r":1, "g":1, "b":1, "a":1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": {"Foo": 10},
"entityCounts": {"Foo": 5},
"entityLimitChangers": {"Foo": {}},
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r":1, "g":0, "b":0, "a":1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": {"Bar": 20},
"entityCounts": {"Bar": 0},
"entityLimitChangers": {"Bar": {}},
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
}
],
"circularMap": false,
"timeElapsed": 0,
- "gameType": "conquest",
+ "victoryConditions": ["conquest", "wonder"],
"alliedVictory": false
});
AddMock(10, IID_Builder, {
GetEntitiesList: function() {
return ["test1", "test2"];
},
});
AddMock(10, IID_Health, {
GetHitpoints: function() { return 50; },
GetMaxHitpoints: function() { return 60; },
IsRepairable: function() { return false; },
IsUnhealable: function() { return false; }
});
AddMock(10, IID_Identity, {
GetClassesList: function() { return ["class1", "class2"]; },
GetVisibleClassesList: function() { return ["class3", "class4"]; },
GetRank: function() { return "foo"; },
GetSelectionGroupName: function() { return "Selection Group Name"; },
HasClass: function() { return true; },
IsUndeletable: function() { return false; }
});
AddMock(10, IID_Position, {
GetTurretParent: function() {return INVALID_ENTITY;},
GetPosition: function() {
return {x:1, y:2, z:3};
},
IsInWorld: function() {
return true;
}
});
AddMock(10, IID_ResourceTrickle, {
"GetTimer": () => 1250,
"GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 })
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
"id": 10,
"player": INVALID_PLAYER,
"template": "example",
"identity": {
"rank": "foo",
"classes": ["class1", "class2"],
"visibleClasses": ["class3", "class4"],
"selectionGroupName": "Selection Group Name",
"canDelete": true
},
"position": {x:1, y:2, z:3},
"hitpoints": 50,
"maxHitpoints": 60,
"needsRepair": false,
"needsHeal": true,
"builder": true,
"canGarrison": false,
"visibility": "visible",
"isBarterMarket":true,
"resourceTrickle": {
"interval": 1250,
"rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 }
}
});
Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/conquest.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/conquest.json (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/conquest.json (revision 21474)
@@ -1,15 +1,20 @@
{
"TranslatedKeys": ["Title", "Description"],
"Data":
{
"Title": "Conquest",
- "Description": "Defeat all opponents to win.",
+ "Description": "Defeat opponents by killing all their units and destroying all their structures.",
"Scripts":
[
"scripts/TriggerHelper.js",
"scripts/ConquestCommon.js",
"scripts/Conquest.js"
],
- "Default": true
+ "Default": true,
+ "ChangeOnChecked": {
+ "conquest_units": false,
+ "conquest_structures": false
+ },
+ "GUIOrder": 0
}
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/conquest_units.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/conquest_units.json (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/conquest_units.json (revision 21474)
@@ -1,14 +1,16 @@
{
"TranslatedKeys": ["Title", "Description"],
"Data":
{
"Title": "Conquest Units",
- "Description": "Kill all enemy units to win.",
+ "Description": "Defeat opponents by killing all their units.",
"Scripts":
[
"scripts/TriggerHelper.js",
"scripts/ConquestCommon.js",
"scripts/ConquestUnits.js"
- ]
+ ],
+ "DisabledWhenChecked": ["conquest"],
+ "GUIOrder": 1
}
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/wonder.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/wonder.json (revision 21473)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/wonder.json (revision 21474)
@@ -1,15 +1,14 @@
{
"TranslatedKeys": ["Title", "Description"],
"Data":
{
"Title": "Wonder",
"Description": "Be the first to build or capture a Wonder and keep it for a certain time to win the game. The timer will be reset when the Wonder is destroyed or captured or in allied victory when the owners alliances change.",
"Scripts":
[
"scripts/TriggerHelper.js",
- "scripts/ConquestCommon.js",
- "scripts/Conquest.js",
"scripts/WonderVictory.js"
- ]
+ ],
+ "GUIOrder": 10
}
}
Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp
===================================================================
--- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 21473)
+++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 21474)
@@ -1,1594 +1,1607 @@
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "lib/app_hooks.h"
#include "lib/config2.h"
#include "lib/input.h"
#include "lib/ogl.h"
#include "lib/timer.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/file/common/file_stats.h"
#include "lib/res/h_mgr.h"
#include "lib/res/graphics/cursor.h"
#include "lib/sysdep/cursor.h"
#include "graphics/CinemaManager.h"
#include "graphics/FontMetrics.h"
#include "graphics/GameView.h"
#include "graphics/LightEnv.h"
#include "graphics/MapReader.h"
#include "graphics/MaterialManager.h"
#include "graphics/TerrainTextureManager.h"
#include "gui/GUI.h"
#include "gui/GUIManager.h"
#include "gui/scripting/ScriptFunctions.h"
#include "i18n/L10n.h"
#include "maths/MathUtil.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetMessages.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/Paths.h"
#include "ps/GameSetup/Config.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/HWDetect.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Joystick.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/Profiler2.h"
#include "ps/Pyrogenesis.h" // psSetLogDir
#include "ps/scripting/JSInterface_Console.h"
#include "ps/TouchInput.h"
#include "ps/UserReport.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "ps/VisualReplay.h"
#include "ps/World.h"
#include "renderer/Renderer.h"
#include "renderer/VertexBufferManager.h"
#include "renderer/ModelRenderer.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptStats.h"
#include "scriptinterface/ScriptConversions.h"
#include "simulation2/Simulation2.h"
#include "lobby/IXmppClient.h"
#include "soundmanager/scripting/JSInterface_Sound.h"
#include "soundmanager/ISoundManager.h"
#include "tools/atlas/GameInterface/GameLoop.h"
#include "tools/atlas/GameInterface/View.h"
#if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets
#define MUST_INIT_X11 1
#include
#else
#define MUST_INIT_X11 0
#endif
extern void restart_engine();
#include
#include
#include
ERROR_GROUP(System);
ERROR_TYPE(System, SDLInitFailed);
ERROR_TYPE(System, VmodeFailed);
ERROR_TYPE(System, RequiredExtensionsMissing);
bool g_DoRenderGui = true;
bool g_DoRenderLogger = true;
bool g_DoRenderCursor = true;
shared_ptr g_ScriptRuntime;
static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code
bool g_InDevelopmentCopy;
bool g_CheckedIfInDevelopmentCopy = false;
static void SetTextureQuality(int quality)
{
int q_flags;
GLint filter;
retry:
// keep this in sync with SANE_TEX_QUALITY_DEFAULT
switch(quality)
{
// worst quality
case 0:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_NEAREST;
break;
// [perf] add bilinear filtering
case 1:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] no longer reduce resolution
case 2:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] add mipmaps
case 3:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_NEAREST_MIPMAP_LINEAR;
break;
// [perf] better filtering
case 4:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [vmem] no longer reduce bpp
case SANE_TEX_QUALITY_DEFAULT:
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [perf] add anisotropy
case 6:
// TODO: add anisotropic filtering
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// invalid
default:
debug_warn(L"SetTextureQuality: invalid quality");
quality = SANE_TEX_QUALITY_DEFAULT;
// careful: recursion doesn't work and we don't want to duplicate
// the "sane" default values.
goto retry;
}
ogl_tex_set_defaults(q_flags, filter);
}
//----------------------------------------------------------------------------
// GUI integration
//----------------------------------------------------------------------------
// display progress / description in loading screen
void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task)
{
g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_Progress", percent, true);
g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_LoadDescription", pending_task, true);
g_GUI->GetActiveGUI()->SendEventToAll("progress");
}
void Render()
{
PROFILE3("render");
if (g_SoundManager)
g_SoundManager->IdleTask();
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameStart();
ogl_WarnIfError();
// prepare before starting the renderer frame
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->BeginFrame();
if (g_Game)
g_Renderer.SetSimulation(g_Game->GetSimulation2());
// start new frame
g_Renderer.BeginFrame();
ogl_WarnIfError();
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->Render();
ogl_WarnIfError();
g_Renderer.RenderTextOverlays();
// If we're in Atlas game view, render special tools
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
g_AtlasGameLoop->view->DrawCinemaPathTool();
ogl_WarnIfError();
}
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->GetCinema()->Render();
ogl_WarnIfError();
if (g_DoRenderGui)
g_GUI->Draw();
ogl_WarnIfError();
// If we're in Atlas game view, render special overlays (e.g. editor bandbox)
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
g_AtlasGameLoop->view->DrawOverlays();
ogl_WarnIfError();
}
// Text:
glDisable(GL_DEPTH_TEST);
g_Console->Render();
ogl_WarnIfError();
if (g_DoRenderLogger)
g_Logger->Render();
ogl_WarnIfError();
// Profile information
g_ProfileViewer.RenderProfile();
ogl_WarnIfError();
// Draw the cursor (or set the Windows cursor, on Windows)
if (g_DoRenderCursor)
{
PROFILE3_GPU("cursor");
CStrW cursorName = g_CursorName;
if (cursorName.empty())
{
cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false);
}
else
{
bool forceGL = false;
CFG_GET_VAL("nohwcursor", forceGL);
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// set up transform for GL cursor
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
CMatrix3D transform;
transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f);
glLoadMatrixf(&transform._11);
#endif
#if OS_ANDROID
#warning TODO: cursors for Android
#else
if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0)
LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName));
#endif
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// restore transform
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
#endif
}
}
glEnable(GL_DEPTH_TEST);
g_Renderer.EndFrame();
PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls);
PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris);
PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris);
PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris);
PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris);
PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats);
PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles);
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameEnd();
ogl_WarnIfError();
}
ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags))
{
// If we're fullscreen, then sometimes (at least on some particular drivers on Linux)
// displaying the error dialog hangs the desktop since the dialog box is behind the
// fullscreen window. So we just force the game to windowed mode before displaying the dialog.
// (But only if we're in the main thread, and not if we're being reentrant.)
if (ThreadUtil::IsMainThread())
{
static bool reentering = false;
if (!reentering)
{
reentering = true;
g_VideoMode.SetFullscreen(false);
reentering = false;
}
}
// We don't actually implement the error display here, so return appropriately
return ERI_NOT_IMPLEMENTED;
}
const std::vector& GetMods(const CmdLineArgs& args, int flags)
{
const bool init_mods = (flags & INIT_MODS) == INIT_MODS;
const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod");
const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC;
if (!init_mods)
{
// Add the user mod if it should be present
if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user"))
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
g_modsLoaded = args.GetMultiple("mod");
if (add_public)
g_modsLoaded.insert(g_modsLoaded.begin(), "public");
g_modsLoaded.insert(g_modsLoaded.begin(), "mod");
// Add the user mod if not explicitly disabled or we have a dev copy so
// that saved files end up in version control and not in the user mod.
if (add_user)
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
void MountMods(const Paths& paths, const std::vector& mods)
{
OsPath modPath = paths.RData()/"mods";
OsPath modUserPath = paths.UserData()/"mods";
for (size_t i = 0; i < mods.size(); ++i)
{
size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0
size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE;
size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST;
OsPath modName(mods[i]);
if (InDevelopmentCopy())
{
// We are running a dev copy, so only mount mods in the user mod path
// if the mod does not exist in the data path.
if (DirectoryExists(modPath / modName/""))
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
else
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority);
}
else
{
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
// Ensure that user modified files are loaded, if they are present
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1);
}
}
}
static void InitVfs(const CmdLineArgs& args, int flags)
{
TIMER(L"InitVfs");
const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0;
const Paths paths(args);
OsPath logs(paths.Logs());
CreateDirectories(logs, 0700);
psSetLogDir(logs);
// desired location for crashlog is now known. update AppHooks ASAP
// (particularly before the following error-prone operations):
AppHooks hooks = {0};
hooks.bundle_logs = psBundleLogs;
hooks.get_log_dir = psLogDir;
if (setup_error)
hooks.display_error = psDisplayError;
app_hooks_update(&hooks);
g_VFS = CreateVfs();
const OsPath readonlyConfig = paths.RData()/"config"/"";
g_VFS->Mount(L"config/", readonlyConfig);
// Engine localization files.
g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/"");
MountMods(paths, GetMods(args, flags));
// We mount these dirs last as otherwise writing could result in files being placed in a mod's dir.
g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"");
g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH);
// Mounting with highest priority, so that a mod supplied user.cfg is harmless
g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1);
if(readonlyConfig != paths.Config())
g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1);
g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads)
// note: don't bother with g_VFS->TextRepresentation - directories
// haven't yet been populated and are empty.
}
static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData)
{
{
// console
TIMER(L"ps_console");
g_Console->UpdateScreenSize(g_xres, g_yres);
// Calculate and store the line spacing
CFontMetrics font(CStrIntern(CONSOLE_FONT));
g_Console->m_iFontHeight = font.GetLineSpacing();
g_Console->m_iFontWidth = font.GetCharacterWidth(L'C');
g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth);
// Offset by an arbitrary amount, to make it fit more nicely
g_Console->m_iFontOffset = 7;
double blinkRate = 0.5;
CFG_GET_VAL("gui.cursorblinkrate", blinkRate);
g_Console->SetCursorBlinkRate(blinkRate);
}
// hotkeys
{
TIMER(L"ps_lang_hotkeys");
LoadHotkeys();
}
if (!setup_gui)
{
// We do actually need *some* kind of GUI loaded, so use the
// (currently empty) Atlas one
g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData);
return;
}
// GUI uses VFS, so this must come after VFS init.
g_GUI->SwitchPage(gui_page, srcScriptInterface, initData);
}
static void InitInput()
{
g_Joystick.Initialise();
// register input handlers
// This stack is constructed so the first added, will be the last
// one called. This is important, because each of the handlers
// has the potential to block events to go further down
// in the chain. I.e. the last one in the list added, is the
// only handler that can block all messages before they are
// processed.
in_add_handler(game_view_handler);
in_add_handler(CProfileViewer::InputThunk);
in_add_handler(conInputHandler);
in_add_handler(HotkeyInputHandler);
// gui_handler needs to be registered after (i.e. called before!) the
// hotkey handler so that input boxes can be typed in without
// setting off hotkeys.
in_add_handler(gui_handler);
in_add_handler(touch_input_handler);
// must be registered after (called before) the GUI which relies on these globals
in_add_handler(GlobalsInputHandler);
}
static void ShutdownPs()
{
SAFE_DELETE(g_GUI);
UnloadHotkeys();
// disable the special Windows cursor, or free textures for OGL cursors
cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false);
}
static void InitRenderer()
{
TIMER(L"InitRenderer");
if(g_NoGLS3TC)
ogl_tex_override(OGL_TEX_S3TC, OGL_TEX_DISABLE);
if(g_NoGLAutoMipmap)
ogl_tex_override(OGL_TEX_AUTO_MIPMAP_GEN, OGL_TEX_DISABLE);
// create renderer
new CRenderer;
// set renderer options from command line options - NOVBO must be set before opening the renderer
// and init them in the ConfigDB when needed
g_Renderer.SetOptionBool(CRenderer::OPT_NOVBO, g_NoGLVBO);
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, g_Shadows);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "shadows", g_Shadows);
g_Renderer.SetOptionBool(CRenderer::OPT_WATEREFFECTS, g_WaterEffects);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "watereffects", g_WaterEffects);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERFANCYEFFECTS, g_WaterFancyEffects);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterfancyeffects", g_WaterFancyEffects);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERREALDEPTH, g_WaterRealDepth);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterrealdepth", g_WaterRealDepth);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFLECTION, g_WaterReflection);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterreflection", g_WaterReflection);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFRACTION, g_WaterRefraction);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterrefraction", g_WaterRefraction);
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWSONWATER, g_WaterShadows);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "watershadows", g_WaterShadows);
g_Renderer.SetRenderPath(CRenderer::GetRenderPathByName(g_RenderPath));
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWPCF, g_ShadowPCF);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "shadowpcf", g_ShadowPCF);
g_Renderer.SetOptionBool(CRenderer::OPT_PARTICLES, g_Particles);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "particles", g_Particles);
g_Renderer.SetOptionBool(CRenderer::OPT_FOG, g_Fog);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "fog", g_Fog);
g_Renderer.SetOptionBool(CRenderer::OPT_SILHOUETTES, g_Silhouettes);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "silhouettes", g_Silhouettes);
g_Renderer.SetOptionBool(CRenderer::OPT_SHOWSKY, g_ShowSky);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "showsky", g_ShowSky);
g_Renderer.SetOptionBool(CRenderer::OPT_PREFERGLSL, g_PreferGLSL);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "preferglsl", g_PreferGLSL);
g_Renderer.SetOptionBool(CRenderer::OPT_POSTPROC, g_PostProc);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "postproc", g_PostProc);
g_Renderer.SetOptionBool(CRenderer::OPT_SMOOTHLOS, g_SmoothLOS);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "smoothlos", g_SmoothLOS);
// create terrain related stuff
new CTerrainTextureManager;
g_Renderer.Open(g_xres, g_yres);
// Setup lighting environment. Since the Renderer accesses the
// lighting environment through a pointer, this has to be done before
// the first Frame.
g_Renderer.SetLightEnv(&g_LightEnv);
// I haven't seen the camera affecting GUI rendering and such, but the
// viewport has to be updated according to the video mode
SViewPort vp;
vp.m_X = 0;
vp.m_Y = 0;
vp.m_Width = g_xres;
vp.m_Height = g_yres;
g_Renderer.SetViewport(vp);
ColorActivateFastImpl();
ModelRenderer::Init();
}
static void InitSDL()
{
#if OS_LINUX
// In fullscreen mode when SDL is compiled with DGA support, the mouse
// sensitivity often appears to be unusably wrong (typically too low).
// (This seems to be reported almost exclusively on Ubuntu, but can be
// reproduced on Gentoo after explicitly enabling DGA.)
// Disabling the DGA mouse appears to fix that problem, and doesn't
// have any obvious negative effects.
setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0);
#endif
if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0)
{
LOGERROR("SDL library initialization failed: %s", SDL_GetError());
throw PSERROR_System_SDLInitFailed();
}
atexit(SDL_Quit);
// Text input is active by default, disable it until it is actually needed.
SDL_StopTextInput();
#if OS_MACOSX
// Some Mac mice only have one button, so they can't right-click
// but SDL2 can emulate that with Ctrl+Click
bool macMouse = false;
CFG_GET_VAL("macmouse", macMouse);
SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0");
#endif
}
static void ShutdownSDL()
{
SDL_Quit();
sys_cursor_reset();
}
void EndGame()
{
const bool nonVisual = g_Game && g_Game->IsGraphicsDisabled();
if (g_Game && g_Game->IsGameStarted() && !g_Game->IsVisualReplay() &&
g_AtlasGameLoop && !g_AtlasGameLoop->running && !nonVisual)
VisualReplay::SaveReplayMetadata(g_GUI->GetActiveGUI()->GetScriptInterface().get());
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_Game);
if (!nonVisual)
{
ISoundManager::CloseGame();
g_Renderer.ResetState();
}
}
void Shutdown(int flags)
{
const bool nonVisual = g_Game && g_Game->IsGraphicsDisabled();
if ((flags & SHUTDOWN_FROM_CONFIG))
goto from_config;
EndGame();
SAFE_DELETE(g_XmppClient);
ShutdownPs();
TIMER_BEGIN(L"shutdown TexMan");
delete &g_TexMan;
TIMER_END(L"shutdown TexMan");
// destroy renderer if it was initialised
if (!nonVisual)
{
TIMER_BEGIN(L"shutdown Renderer");
delete &g_Renderer;
g_VBMan.Shutdown();
TIMER_END(L"shutdown Renderer");
}
g_Profiler2.ShutdownGPU();
// Free cursors before shutting down SDL, as they may depend on SDL.
cursor_shutdown();
TIMER_BEGIN(L"shutdown SDL");
ShutdownSDL();
TIMER_END(L"shutdown SDL");
if (!nonVisual)
g_VideoMode.Shutdown();
TIMER_BEGIN(L"shutdown UserReporter");
g_UserReporter.Deinitialize();
TIMER_END(L"shutdown UserReporter");
delete &g_L10n;
from_config:
TIMER_BEGIN(L"shutdown ConfigDB");
delete &g_ConfigDB;
TIMER_END(L"shutdown ConfigDB");
SAFE_DELETE(g_Console);
// This is needed to ensure that no callbacks from the JSAPI try to use
// the profiler when it's already destructed
g_ScriptRuntime.reset();
// resource
// first shut down all resource owners, and then the handle manager.
TIMER_BEGIN(L"resource modules");
ISoundManager::SetEnabled(false);
g_VFS.reset();
// this forcibly frees all open handles (thus preventing real leaks),
// and makes further access to h_mgr impossible.
h_mgr_shutdown();
file_stats_dump();
TIMER_END(L"resource modules");
TIMER_BEGIN(L"shutdown misc");
timer_DisplayClientTotals();
CNetHost::Deinitialize();
// should be last, since the above use them
SAFE_DELETE(g_Logger);
delete &g_Profiler;
delete &g_ProfileViewer;
SAFE_DELETE(g_ScriptStatsTable);
TIMER_END(L"shutdown misc");
}
#if OS_UNIX
static void FixLocales()
{
#if OS_MACOSX || OS_BSD
// OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle
// wide characters. Peculiarly the string "UTF-8" seems to be acceptable
// despite not being a real locale, and it's conveniently language-agnostic,
// so use that.
setlocale(LC_CTYPE, "UTF-8");
#endif
// On misconfigured systems with incorrect locale settings, we'll die
// with a C++ exception when some code (e.g. Boost) tries to use locales.
// To avoid death, we'll detect the problem here and warn the user and
// reset to the default C locale.
// For informing the user of the problem, use the list of env vars that
// glibc setlocale looks at. (LC_ALL is checked first, and LANG last.)
const char* const LocaleEnvVars[] = {
"LC_ALL",
"LC_COLLATE",
"LC_CTYPE",
"LC_MONETARY",
"LC_NUMERIC",
"LC_TIME",
"LC_MESSAGES",
"LANG"
};
try
{
// this constructor is similar to setlocale(LC_ALL, ""),
// but instead of returning NULL, it throws runtime_error
// when the first locale env variable found contains an invalid value
std::locale("");
}
catch (std::runtime_error&)
{
LOGWARNING("Invalid locale settings");
for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++)
{
if (char* envval = getenv(LocaleEnvVars[i]))
LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval);
else
LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]);
}
// We should set LC_ALL since it overrides LANG
if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1))
debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable.");
else
LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL"));
}
}
#else
static void FixLocales()
{
// Do nothing on Windows
}
#endif
void EarlyInit()
{
// If you ever want to catch a particular allocation:
//_CrtSetBreakAlloc(232647);
ThreadUtil::SetMainThread();
debug_SetThreadName("main");
// add all debug_printf "tags" that we are interested in:
debug_filter_add("TIMER");
timer_LatchStartTime();
// initialise profiler early so it can profile startup,
// but only after LatchStartTime
g_Profiler2.Initialise();
FixLocales();
// Because we do GL calls from a secondary thread, Xlib needs to
// be told to support multiple threads safely.
// This is needed for Atlas, but we have to call it before any other
// Xlib functions (e.g. the ones used when drawing the main menu
// before launching Atlas)
#if MUST_INIT_X11
int status = XInitThreads();
if (status == 0)
debug_printf("Error enabling thread-safety via XInitThreads\n");
#endif
// Initialise the low-quality rand function
srand(time(NULL)); // NOTE: this rand should *not* be used for simulation!
}
bool Autostart(const CmdLineArgs& args);
/**
* Returns true if the user has intended to start a visual replay from command line.
*/
bool AutostartVisualReplay(const std::string& replayFile);
bool Init(const CmdLineArgs& args, int flags)
{
h_mgr_init();
// Do this as soon as possible, because it chdirs
// and will mess up the error reporting if anything
// crashes before the working directory is set.
InitVfs(args, flags);
// This must come after VFS init, which sets the current directory
// (required for finding our output log files).
g_Logger = new CLogger;
new CProfileViewer;
new CProfileManager; // before any script code
g_ScriptStatsTable = new CScriptStatsTable;
g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
// Set up the console early, so that debugging
// messages can be logged to it. (The console's size
// and fonts are set later in InitPs())
g_Console = new CConsole();
// g_ConfigDB, command line args, globals
CONFIG_Init(args);
// Using a global object for the runtime is a workaround until Simulation and AI use
// their own threads and also their own runtimes.
const int runtimeSize = 384 * 1024 * 1024;
const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger);
// Special command-line mode to dump the entity schemas instead of running the game.
// (This must be done after loading VFS etc, but should be done before wasting time
// on anything else.)
if (args.Has("dumpSchema"))
{
CSimulation2 sim(NULL, g_ScriptRuntime, NULL);
sim.LoadDefaultScripts();
std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc);
f << sim.GenerateSchema();
std::cout << "Generated entity.rng\n";
exit(0);
}
CNetHost::Initialize();
#if CONFIG2_AUDIO
if (!args.Has("autostart-nonvisual"))
ISoundManager::CreateSoundManager();
#endif
// Check if there are mods specified on the command line,
// or if we already set the mods (~INIT_MODS),
// else check if there are mods that should be loaded specified
// in the config and load those (by aborting init and restarting
// the engine).
if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS)
{
CStr modstring;
CFG_GET_VAL("mod.enabledmods", modstring);
if (!modstring.empty())
{
std::vector mods;
boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on);
std::swap(g_modsLoaded, mods);
// Abort init and restart
restart_engine();
return false;
}
}
new L10n;
// Optionally start profiler HTTP output automatically
// (By default it's only enabled by a hotkey, for security/performance)
bool profilerHTTPEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable);
if (profilerHTTPEnable)
g_Profiler2.EnableHTTP();
if (!g_Quickstart)
g_UserReporter.Initialize(); // after config
PROFILE2_EVENT("Init finished");
return true;
}
void InitGraphics(const CmdLineArgs& args, int flags)
{
const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0;
if(setup_vmode)
{
InitSDL();
if (!g_VideoMode.InitSDL())
throw PSERROR_System_VmodeFailed(); // abort startup
}
RunHardwareDetection();
const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file
SetTextureQuality(quality);
ogl_WarnIfError();
// Optionally start profiler GPU timings automatically
// (By default it's only enabled by a hotkey, for performance/compatibility)
bool profilerGPUEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable);
if (profilerGPUEnable)
g_Profiler2.EnableGPU();
if(!g_Quickstart)
{
WriteSystemInfo();
// note: no longer vfs_display here. it's dog-slow due to unbuffered
// file output and very rarely needed.
}
if(g_DisableAudio)
ISoundManager::SetEnabled(false);
g_GUI = new CGUIManager();
// (must come after SetVideoMode, since it calls ogl_Init)
if (ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB
&& ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL
{
DEBUG_DISPLAY_ERROR(
L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders."
L" In the future, the game will not support pre-shader graphics cards."
L" You are advised to try installing newer drivers and/or upgrade your graphics card."
L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734"
);
// TODO: actually quit once fixed function support is dropped
}
const char* missing = ogl_HaveExtensions(0,
"GL_ARB_multitexture",
"GL_EXT_draw_range_elements",
"GL_ARB_texture_env_combine",
"GL_ARB_texture_env_dot3",
NULL);
if(missing)
{
wchar_t buf[500];
swprintf_s(buf, ARRAY_SIZE(buf),
L"The %hs extension doesn't appear to be available on your computer."
L" The game may still work, though - you are welcome to try at your own risk."
L" If not or it doesn't look right, upgrade your graphics card.",
missing
);
DEBUG_DISPLAY_ERROR(buf);
// TODO: i18n
}
if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar"))
{
DEBUG_DISPLAY_ERROR(
L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer."
L" Shadows are not available and overall graphics quality might suffer."
L" You are advised to try installing newer drivers and/or upgrade your graphics card.");
g_Shadows = false;
}
ogl_WarnIfError();
InitRenderer();
InitInput();
ogl_WarnIfError();
// TODO: Is this the best place for this?
if (VfsDirectoryExists(L"maps/"))
CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng");
try
{
if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args))
{
const bool setup_gui = ((flags & INIT_NO_GUI) == 0);
// We only want to display the splash screen at startup
shared_ptr scriptInterface = g_GUI->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue data(cx);
if (g_GUI)
{
scriptInterface->Eval("({})", &data);
scriptInterface->SetProperty(data, "isStartup", true);
}
InitPs(setup_gui, L"page_pregame.xml", g_GUI->GetScriptInterface().get(), data);
}
}
catch (PSERROR_Game_World_MapLoadFailed& e)
{
// Map Loading failed
// Start the engine so we have a GUI
InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue);
// Call script function to do the actual work
// (delete game data, switch GUI page, show error, etc.)
CancelLoad(CStr(e.what()).FromUTF8());
}
}
void InitNonVisual(const CmdLineArgs& args)
{
// Need some stuff for terrain movement costs:
// (TODO: this ought to be independent of any graphics code)
new CTerrainTextureManager;
g_TexMan.LoadTerrainTextures();
Autostart(args);
}
void RenderGui(bool RenderingState)
{
g_DoRenderGui = RenderingState;
}
void RenderLogger(bool RenderingState)
{
g_DoRenderLogger = RenderingState;
}
void RenderCursor(bool RenderingState)
{
g_DoRenderCursor = RenderingState;
}
/**
* Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON
* data from it.
* The scenario map format is used for scenario and skirmish map types (random
* games do not use a "map" (format) but a small JavaScript program which
* creates a map on the fly). It contains a section to initialize the game
* setup screen.
* @param mapPath Absolute path (from VFS root) to the map file to peek in.
* @return ScriptSettings in JSON format extracted from the map.
*/
CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath)
{
CXeromyces mapFile;
const char *pathToSettings[] =
{
"Scenario", "ScriptSettings", "" // Path to JSON data in map
};
Status loadResult = mapFile.Load(g_VFS, mapPath);
if (INFO::OK != loadResult)
{
LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8());
throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos.");
}
XMBElement mapElement = mapFile.GetRoot();
// Select the ScriptSettings node in the map file...
for (int i = 0; pathToSettings[i][0]; ++i)
{
int childId = mapFile.GetElementID(pathToSettings[i]);
XMBElementList nodes = mapElement.GetChildNodes();
auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) {
return child.GetNodeName() == childId;
});
if (it != nodes.end())
mapElement = *it;
}
// ... they contain a JSON document to initialize the game setup
// screen
return mapElement.GetText();
}
/*
* Command line options for autostart
* (keep synchronized with binaries/system/readme.txt):
*
* -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME;
* TYPEDIR is skirmishes, scenarios, or random
* -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random)
* -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra)
* -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI
* (0: sandbox, 5: very hard)
* -autostart-aiseed=AISEED sets the seed used for the AI random
* generator (default 0, use -1 for random)
* -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV
* (skirmish and random maps only)
* -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2).
* -autostart-nonvisual disable any graphics and sounds
* -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME
* located in simulation/data/settings/victory_conditions/
* (default conquest)
* -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory conditions
* (default 10 minutes)
* -autostart-relicduration=NUM sets the victory duration NUM for relic victory conditions
* (default 10 minutes)
*
* Multiplayer:
* -autostart-playername=NAME sets local player NAME (default 'anonymous')
* -autostart-host sets multiplayer host mode
* -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer
* game (default 2)
* -autostart-client=IP sets multiplayer client to join host at
* given IP address
* Random maps only:
* -autostart-size=TILES sets random map size in TILES (default 192)
* -autostart-players=NUMBER sets NUMBER of players on random map
* (default 2)
*
* Examples:
* 1) "Bob" will host a 2 player game on the Arcadia map:
* -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob"
*
* 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot:
* -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra
*/
bool Autostart(const CmdLineArgs& args)
{
CStr autoStartName = args.Get("autostart");
if (autoStartName.empty())
return false;
const bool nonVisual = args.Has("autostart-nonvisual");
g_Game = new CGame(nonVisual, !nonVisual);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attrs(cx);
scriptInterface.Eval("({})", &attrs);
JS::RootedValue settings(cx);
scriptInterface.Eval("({})", &settings);
JS::RootedValue playerData(cx);
scriptInterface.Eval("([])", &playerData);
// The directory in front of the actual map name indicates which type
// of map is being loaded. Drawback of this approach is the association
// of map types and folders is hard-coded, but benefits are:
// - No need to pass the map type via command line separately
// - Prevents mixing up of scenarios and skirmish maps to some degree
Path mapPath = Path(autoStartName);
std::wstring mapDirectory = mapPath.Parent().Filename().string();
std::string mapType;
if (mapDirectory == L"random")
{
// Random map definition will be loaded from JSON file, so we need to parse it
std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json";
JS::RootedValue scriptData(cx);
scriptInterface.ReadJSONFile(scriptPath, &scriptData);
if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings))
{
// JSON loaded ok - copy script name over to game attributes
std::wstring scriptFile;
scriptInterface.GetProperty(settings, "Script", scriptFile);
scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename
}
else
{
// Problem with JSON file
LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath));
throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details.");
}
// Get optional map size argument (default 192)
uint mapSize = 192;
if (args.Has("autostart-size"))
{
CStr size = args.Get("autostart-size");
mapSize = size.ToUInt();
}
scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches)
// Get optional number of players (default 2)
size_t numPlayers = 2;
if (args.Has("autostart-players"))
{
CStr num = args.Get("autostart-players");
numPlayers = num.ToUInt();
}
// Set up player data
for (size_t i = 0; i < numPlayers; ++i)
{
JS::RootedValue player(cx);
scriptInterface.Eval("({})", &player);
// We could load player_defaults.json here, but that would complicate the logic
// even more and autostart is only intended for developers anyway
scriptInterface.SetProperty(player, "Civ", std::string("athen"));
scriptInterface.SetPropertyInt(playerData, i, player);
}
mapType = "random";
}
else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// Initialize general settings from the map data so some values
// (e.g. name of map) are always present, even when autostart is
// partially configured
CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml");
scriptInterface.ParseJSON(mapSettingsJSON, &settings);
// Initialize the playerData array being modified by autostart
// with the real map data, so sensible values are present:
scriptInterface.GetProperty(settings, "PlayerData", &playerData);
if (mapDirectory == L"scenarios")
mapType = "scenario";
else
mapType = "skirmish";
}
else
{
LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory));
throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types.");
}
scriptInterface.SetProperty(attrs, "mapType", mapType);
scriptInterface.SetProperty(attrs, "map", std::string("maps/" + autoStartName));
scriptInterface.SetProperty(settings, "mapType", mapType);
scriptInterface.SetProperty(settings, "CheatsEnabled", true);
// The seed is used for both random map generation and simulation
u32 seed = 0;
if (args.Has("autostart-seed"))
{
CStr seedArg = args.Get("autostart-seed");
if (seedArg == "-1")
seed = rand();
else
seed = seedArg.ToULong();
}
scriptInterface.SetProperty(settings, "Seed", seed);
// Set seed for AIs
u32 aiseed = 0;
if (args.Has("autostart-aiseed"))
{
CStr seedArg = args.Get("autostart-aiseed");
if (seedArg == "-1")
aiseed = rand();
else
aiseed = seedArg.ToULong();
}
scriptInterface.SetProperty(settings, "AISeed", aiseed);
// Set player data for AIs
// attrs.settings = { PlayerData: [ { AI: ... }, ... ] }
// or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set
int offset = 1;
JS::RootedValue player(cx);
if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull())
offset = 0;
// Set teams
if (args.Has("autostart-team"))
{
std::vector civArgs = args.GetMultiple("autostart-team");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
int teamID = civArgs[i].AfterFirst(":").ToInt() - 1;
scriptInterface.SetProperty(player, "Team", teamID);
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
if (args.Has("autostart-ai"))
{
std::vector aiArgs = args.GetMultiple("autostart-ai");
for (size_t i = 0; i < aiArgs.size(); ++i)
{
int playerID = aiArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
CStr name = aiArgs[i].AfterFirst(":");
scriptInterface.SetProperty(player, "AI", std::string(name));
scriptInterface.SetProperty(player, "AIDiff", 3);
scriptInterface.SetProperty(player, "AIBehavior", std::string("balanced"));
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
// Set AI difficulty
if (args.Has("autostart-aidiff"))
{
std::vector civArgs = args.GetMultiple("autostart-aidiff");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
int difficulty = civArgs[i].AfterFirst(":").ToInt();
scriptInterface.SetProperty(player, "AIDiff", difficulty);
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
// Set player data for Civs
if (args.Has("autostart-civ"))
{
if (mapDirectory != L"scenarios")
{
std::vector civArgs = args.GetMultiple("autostart-civ");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
CStr name = civArgs[i].AfterFirst(":");
scriptInterface.SetProperty(player, "Civ", std::string(name));
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
else
LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios");
}
// Add player data to map settings
scriptInterface.SetProperty(settings, "PlayerData", playerData);
// Add map settings to game attributes
scriptInterface.SetProperty(attrs, "settings", settings);
JS::RootedValue mpInitData(cx);
scriptInterface.Eval("({isNetworked:true, playerAssignments:{}})", &mpInitData);
scriptInterface.SetProperty(mpInitData, "attribs", attrs);
// Get optional playername
CStrW userName = L"anonymous";
if (args.Has("autostart-playername"))
userName = args.Get("autostart-playername").FromUTF8();
// Add additional scripts to the TriggerScripts property
std::vector triggerScriptsVector;
JS::RootedValue triggerScripts(cx);
if (scriptInterface.HasProperty(settings, "TriggerScripts"))
{
scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts);
FromJSVal_vector(cx, triggerScripts, triggerScriptsVector);
}
if (nonVisual)
{
CStr nonVisualScript = "scripts/NonVisualTrigger.js";
triggerScriptsVector.push_back(nonVisualScript.FromUTF8());
}
- CStr victory = "conquest";
+ std::vector victoryConditions(1, "conquest");
if (args.Has("autostart-victory"))
- victory = args.Get("autostart-victory");
+ victoryConditions = args.GetMultiple("autostart-victory");
- scriptInterface.SetProperty(settings, "GameType", std::string(victory));
+ if (victoryConditions.size() == 1 && victoryConditions[0] == "endless")
+ victoryConditions.clear();
- CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json";
- JS::RootedValue scriptData(cx);
- JS::RootedValue data(cx);
- JS::RootedValue victoryScripts(cx);
- scriptInterface.ReadJSONFile(scriptPath, &scriptData);
- if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined()
- && scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined())
- {
- std::vector victoryScriptsVector;
- FromJSVal_vector(cx, victoryScripts, victoryScriptsVector);
- triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end());
+ scriptInterface.SetProperty(settings, "VictoryConditions", victoryConditions);
+
+ for (const CStr& victory : victoryConditions)
+ {
+ JS::RootedValue scriptData(cx);
+ JS::RootedValue data(cx);
+ JS::RootedValue victoryScripts(cx);
+
+ CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json";
+ scriptInterface.ReadJSONFile(scriptPath, &scriptData);
+ if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined()
+ && scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined())
+ {
+ std::vector victoryScriptsVector;
+ FromJSVal_vector(cx, victoryScripts, victoryScriptsVector);
+ triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end());
+ }
+ else
+ {
+ LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath));
+ throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details.");
+ }
}
+
ToJSVal_vector(cx, &triggerScripts, triggerScriptsVector);
scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts);
int wonderDuration = 10;
if (args.Has("autostart-wonderduration"))
wonderDuration = args.Get("autostart-wonderduration").ToInt();
scriptInterface.SetProperty(settings, "WonderDuration", wonderDuration);
int relicDuration = 10;
if (args.Has("autostart-relicduration"))
relicDuration = args.Get("autostart-relicduration").ToInt();
scriptInterface.SetProperty(settings, "RelicDuration", relicDuration);
if (args.Has("autostart-host"))
{
InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData);
size_t maxPlayers = 2;
if (args.Has("autostart-host-players"))
maxPlayers = args.Get("autostart-host-players").ToUInt();
g_NetServer = new CNetServer(maxPlayers);
g_NetServer->UpdateGameAttributes(&attrs, scriptInterface);
bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT);
ENSURE(ok);
g_NetClient = new CNetClient(g_Game, true);
g_NetClient->SetUserName(userName);
g_NetClient->SetupConnection("127.0.0.1", PS_DEFAULT_PORT);
}
else if (args.Has("autostart-client"))
{
InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData);
g_NetClient = new CNetClient(g_Game, false);
g_NetClient->SetUserName(userName);
CStr ip = args.Get("autostart-client");
if (ip.empty())
ip = "127.0.0.1";
bool ok = g_NetClient->SetupConnection(ip, PS_DEFAULT_PORT);
ENSURE(ok);
}
else
{
g_Game->SetPlayerID(1);
g_Game->StartGame(&attrs, "");
LDR_NonprogressiveLoad();
PSRETURN ret = g_Game->ReallyStartGame();
ENSURE(ret == PSRETURN_OK);
if (nonVisual)
return true;
InitPs(true, L"page_session.xml", NULL, JS::UndefinedHandleValue);
}
return true;
}
bool AutostartVisualReplay(const std::string& replayFile)
{
if (!FileExists(OsPath(replayFile)))
return false;
g_Game = new CGame(false, false);
g_Game->SetPlayerID(-1);
g_Game->StartVisualReplay(replayFile);
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
InitPs(true, L"page_session.xml", &scriptInterface, JS::UndefinedHandleValue);
return true;
}
void CancelLoad(const CStrW& message)
{
shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface();
JSContext* cx = pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, pScriptInterface->GetGlobalObject());
LDR_Cancel();
if (g_GUI &&
g_GUI->HasPages() &&
pScriptInterface->HasProperty(global, "cancelOnLoadGameError"))
pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message);
}
bool InDevelopmentCopy()
{
if (!g_CheckedIfInDevelopmentCopy)
{
g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK);
g_CheckedIfInDevelopmentCopy = true;
}
return g_InDevelopmentCopy;
}