Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/wonder_times.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/settings/wonder_times.json (revision 19344)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/wonder_times.json (nonexistent)
@@ -1,4 +0,0 @@
-{
- "Times": [0, 1, 3, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 75, 90, 105, 120],
- "Default": 20
-}
Property changes on: ps/trunk/binaries/data/mods/public/simulation/data/settings/wonder_times.json
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 19344)
+++ ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 19345)
@@ -1,352 +1,364 @@
/**
* Highlights the victory condition in the game-description.
*/
var g_DescriptionHighlight = "orange";
/**
* 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 {
"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, %(AIdifficulty)s %(AIname)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)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 (%(AIdifficulty)s %(AIname)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIname)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] = [];
playerDescriptions[teamIdx].push(sprintf(playerDescription, {
"playerName":
'[color="' +
(typeof getPlayerColor == 'function' ?
(isAI ? "white" : getPlayerColor(playerData.Name)) :
rgbToGuiColor(playerData.Color || g_Settings.PlayerDefaults[playerIdx].Color)) +
'"]' + escapeText(playerData.Name) + "[/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"),
"AIname": isAI ? translateAIName(playerData.AI) : "",
"AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : ""
}));
}
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 = [];
let victoryIdx = g_VictoryConditions.Name.indexOf(g_GameAttributes.settings.GameType || g_VictoryConditions.Default);
if (victoryIdx != -1)
{
let title = g_VictoryConditions.Title[victoryIdx];
if (g_VictoryConditions.Name[victoryIdx] == "wonder")
title = sprintf(
translatePluralWithContext(
"victory condition",
"Wonder (%(min)s minute)",
"Wonder (%(min)s minutes)",
- g_GameAttributes.settings.WonderDuration
+ g_GameAttributes.settings.VictoryDuration
),
- { "min": g_GameAttributes.settings.WonderDuration }
+ { "min": g_GameAttributes.settings.VictoryDuration }
);
+
+ else if (g_VictoryConditions.Name[victoryIdx] == "capture_the_relic")
+ title = sprintf(
+ translatePluralWithContext(
+ "victory condition",
+ "Capture The Relic (%(min)s minute)",
+ "Capture The Relic (%(min)s minutes)",
+ g_GameAttributes.settings.VictoryDuration
+ ),
+ { "min": g_GameAttributes.settings.VictoryDuration }
+ );
+
titles.push({
"label": title,
"value": g_VictoryConditions.Description[victoryIdx]
});
}
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, enemies will stay neutral.",
"For the first %(min)s minutes, enemies 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)
{
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("Disable Treasure"),
"value": g_GameAttributes.settings.DisableTreasure
});
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": "[color=\"" + g_DescriptionHighlight + "\"]" + title.label + ":" + "[/color]",
"details":
title.value === true ? translateWithContext("gamesetup option", "enabled") :
!title.value ? translateWithContext("gamesetup option", "disabled") :
title.value
})).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");
}
}
Index: ps/trunk/binaries/data/mods/public/gui/common/settings.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 19344)
+++ ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 19345)
@@ -1,378 +1,378 @@
/**
* 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/";
/**
* 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(),
"Ceasefire": loadCeasefire(),
- "WonderDurations": loadWonderDuration(),
+ "VictoryDurations": loadVictoryDuration(),
"GameSpeeds": loadSettingValuesFile("game_speeds.json"),
"MapTypes": loadMapTypes(),
"MapSizes": loadSettingValuesFile("map_sizes.json"),
"PlayerDefaults": loadPlayerDefaults(),
"PopulationCapacities": loadPopulationCapacities(),
"StartingResources": loadSettingValuesFile("starting_resources.json"),
"VictoryConditions": loadVictoryConditions()
};
if (Object.keys(settings).some(key => settings[key] === undefined))
return undefined;
return 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")
}
];
}
/**
- * Loads available wonder-victory times
+ * Loads available victory times for victory conditions like Wonder and Capture The Relic.
*/
-function loadWonderDuration()
+function loadVictoryDuration()
{
- var jsonFile = "wonder_times.json";
+ 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("wonder victory", "%(min)s minute", "%(min)s minutes", duration), { "min": duration })
+ "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"),
"Default": true
},
{
"Name": "random",
"Title": translateWithContext("map", "Random")
},
{
"Name": "scenario",
"Title": translateWithContext("map", "Scenario")
}
];
}
/**
* Loads available gametypes.
*
* @returns {Array|undefined}
*/
function loadVictoryConditions()
{
const subdir = "victory_conditions/";
const files = Engine.BuildDirEntList(g_SettingsDirectory + subdir, "*.json", false).map(
file => file.substr(g_SettingsDirectory.length));
var victoryConditions = files.map(file => {
let vc = loadSettingValuesFile(file);
if (vc)
vc.Name = file.substr(subdir.length, file.length - (subdir + ".json").length);
return vc;
});
if (victoryConditions.some(vc => vc == 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;
var 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 settings;
}
/**
* Returns title or placeholder.
*
* @param {string} aiName - for example "petra"
*/
function translateAIName(aiName)
{
var 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)
{
var difficulty = g_Settings.AIDifficulties[index];
return difficulty ? difficulty.Title : translateWithContext("AI difficulty", "Unknown");
}
/**
* Returns title or placeholder.
*
* @param {string} mapType - for example "skirmish"
* @returns {string}
*/
function translateMapType(mapType)
{
var 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)
{
var mapSize = g_Settings.MapSizes.find(mapSize => mapSize.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)
{
var 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"
* @returns {string}
*/
function translateVictoryCondition(gameType)
{
var vc = g_Settings.VictoryConditions.find(vc => vc.Name == gameType);
return vc ? vc.Title : translateWithContext("victory condition", "Unknown");
}
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 19344)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 19345)
@@ -1,2040 +1,2040 @@
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_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly));
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_WonderDurations = prepareForDropdown(g_Settings && g_Settings.WonderDurations);
+const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
/**
* All selectable playercolors except gaia.
*/
const g_PlayerColors = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
/**
* Directory containing all maps of the given type.
*/
const 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
*/
const g_ReadyData = [
{
"color": "",
"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.
*/
const 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 })
};
const 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()
};
/**
* The dropdownlist items will appear in the order they are added.
*/
const g_MapFilters = [
{
"id": "default",
"name": translateWithContext("map filter", "Default"),
"filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1)
},
{
"id": "naval",
"name": translate("Naval Maps"),
"filter": mapKeywords => mapKeywords.indexOf("naval") != -1
},
{
"id": "demo",
"name": translate("Demo Maps"),
"filter": mapKeywords => mapKeywords.indexOf("demo") != -1
},
{
"id": "new",
"name": translate("New Maps"),
"filter": mapKeywords => mapKeywords.indexOf("new") != -1
},
{
"id": "trigger",
"name": translate("Trigger Maps"),
"filter": mapKeywords => mapKeywords.indexOf("trigger") != -1
},
{
"id": "all",
"name": translate("All Maps"),
"filter": mapKeywords => true
}
];
/**
* Used for generating the botnames.
*/
const g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
/**
* Offer users to select playable civs only.
* Load unselectable civs as they could appear in scenario maps.
*/
const g_CivData = loadCivData();
/**
* Used for highlighting the sender of chat messages.
*/
const g_SenderFont = "sans-bold-13";
/**
* Highlight the "random" dropdownlist item.
*/
const g_ColorRandom = "orange";
/**
* Highlight AIs in the player-dropdownlist.
*/
const g_AIColor = "70 150 70";
/**
* Color for "Unassigned"-placeholder item in the dropdownlist.
*/
const g_UnassignedColor = "140 140 140";
/**
* Highlight observer players in the dropdownlist.
*/
const g_UnassignedPlayerColor = "170 170 250";
/**
* Placeholder item for the map-dropdownlist.
*/
const g_RandomMap = '[color="' + g_ColorRandom + '"]' + translateWithContext("map selection", "Random") + "[/color]";
/**
* Placeholder item for the civ-dropdownlists.
*/
const g_RandomCiv = '[color="' + g_ColorRandom + '"]' + translateWithContext("civilization", "Random") + '[/color]';
/**
* 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;
/**
* To report the game to the lobby bot.
*/
var g_ServerName;
var g_ServerPort;
/**
* 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;
/**
* Whether the current player is ready to start the game.
* 0 - not ready
* 1 - ready
* 2 - stay ready
*/
var g_IsReady;
/**
* 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;
var g_PlayerAssignments = {};
var g_DefaultPlayerData = [];
var g_GameAttributes = { "settings": {} };
var g_ChatMessages = [];
/**
* Cache containing the mapsettings for scenario/skirmish maps. 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;
/**
* 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;
/**
* 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_ServerName = attribs.serverName;
g_ServerPort = attribs.serverPort;
// Replace empty playername when entering a singleplayermatch for the first time
if (!g_IsNetworked)
{
Engine.ConfigDB_CreateValue("user", "playername.singleplayer", singleplayerName());
Engine.ConfigDB_WriteValueToFile("user", "playername.singleplayer", singleplayerName(), "config/user.cfg");
}
// Get default player data - remove gaia
g_DefaultPlayerData = g_Settings.PlayerDefaults;
g_DefaultPlayerData.shift();
for (let i in g_DefaultPlayerData)
g_DefaultPlayerData[i].Civ = "random";
setTimeout(displayGamestateNotifications, 1000);
}
/**
* Called after the first tick.
*/
function initGUIObjects()
{
Engine.GetGUIObjectByName("cancelGame").tooltip = Engine.HasXmppClient() ? translate("Return to the lobby.") : translate("Return to the main menu.");
initCivNameList();
initMapTypes();
initMapFilters();
if (g_IsController)
{
g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked;
g_GameAttributes.settings.RatingEnabled = Engine.IsRankedGame() || undefined;
initMapNameList();
initNumberOfPlayers();
initGameSpeed();
initPopulationCaps();
initStartingResources();
initCeasefire();
- initWonderDurations();
initVictoryConditions();
+ initVictoryDurations();
initMapSizes();
initRadioButtons();
}
else
hideControls();
initMultiplayerSettings();
initPlayerAssignments();
resizeMoreOptionsWindow();
Engine.GetGUIObjectByName("chatInput").tooltip = colorizeAutocompleteHotkey();
if (g_IsNetworked)
Engine.GetGUIObjectByName("chatInput").focus();
else
initSPTips();
if (g_IsController)
{
loadPersistMatchSettings();
if (g_IsInGuiUpdate)
warn("initGUIObjects() called while in GUI update");
updateGameAttributes();
}
}
function initMapTypes()
{
let mapTypes = Engine.GetGUIObjectByName("mapType");
mapTypes.list = g_MapTypes.Title;
mapTypes.list_data = g_MapTypes.Name;
mapTypes.onSelectionChange = function() {
if (this.selected != -1)
selectMapType(this.list_data[this.selected]);
};
if (g_IsController)
mapTypes.selected = g_MapTypes.Default;
}
function initMapFilters()
{
let mapFilters = Engine.GetGUIObjectByName("mapFilter");
mapFilters.list = g_MapFilters.map(mapFilter => mapFilter.name);
mapFilters.list_data = g_MapFilters.map(mapFilter => mapFilter.id);
mapFilters.onSelectionChange = function() {
if (this.selected != -1)
selectMapFilter(this.list_data[this.selected]);
};
if (g_IsController)
mapFilters.selected = 0;
g_GameAttributes.mapFilter = "default";
}
function initSPTips()
{
if (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"));
}
function saveSPTipsSetting()
{
let enabled = String(Engine.GetGUIObjectByName("displaySPTips").checked);
Engine.ConfigDB_CreateValue("user", "gui.gamesetup.enabletips", enabled);
Engine.ConfigDB_WriteValueToFile("user", "gui.gamesetup.enabletips", enabled, "config/user.cfg");
}
/**
- * Remove empty space in case of hidden options (like cheats, rating or wonder duration)
+ * Remove empty space in case of hidden options (like cheats, rating or victory duration)
*/
function resizeMoreOptionsWindow()
{
const elementHeight = 30;
let yPos = undefined;
for (let guiOption of Engine.GetGUIObjectByName("moreOptions").children)
{
if (guiOption.name == "moreOptionsLabel")
continue;
let gSize = guiOption.size;
yPos = yPos || gSize.top;
if (guiOption.hidden)
continue;
gSize.top = yPos;
gSize.bottom = yPos + elementHeight - 2;
guiOption.size = gSize;
yPos += elementHeight;
}
// Resize the vertically centered window containing the options
let moreOptions = Engine.GetGUIObjectByName("moreOptions");
let mSize = moreOptions.size;
mSize.bottom = mSize.top + yPos + 20;
moreOptions.size = mSize;
}
function initNumberOfPlayers()
{
let playersArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ..., MaxPlayers
let numPlayers = Engine.GetGUIObjectByName("numPlayers");
numPlayers.list = playersArray;
numPlayers.list_data = playersArray;
numPlayers.onSelectionChange = function() {
if (this.selected != -1)
selectNumPlayers(this.list_data[this.selected]);
};
numPlayers.selected = g_MaxPlayers - 1;
}
function initGameSpeed()
{
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.hidden = false;
Engine.GetGUIObjectByName("gameSpeedText").hidden = true;
gameSpeed.list = g_GameSpeeds.Title;
gameSpeed.list_data = g_GameSpeeds.Speed;
gameSpeed.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[this.selected];
updateGameAttributes();
};
gameSpeed.selected = g_GameSpeeds.Default;
}
function initPopulationCaps()
{
let populationCaps = Engine.GetGUIObjectByName("populationCap");
populationCaps.list = g_PopulationCapacities.Title;
populationCaps.list_data = g_PopulationCapacities.Population;
populationCaps.selected = g_PopulationCapacities.Default;
populationCaps.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[this.selected];
updateGameAttributes();
};
}
function initStartingResources()
{
let startingResourcesL = Engine.GetGUIObjectByName("startingResources");
startingResourcesL.list = g_StartingResources.Title;
startingResourcesL.list_data = g_StartingResources.Resources;
startingResourcesL.selected = g_StartingResources.Default;
startingResourcesL.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[this.selected];
updateGameAttributes();
};
}
function initCeasefire()
{
let ceasefireL = Engine.GetGUIObjectByName("ceasefire");
ceasefireL.list = g_Ceasefire.Title;
ceasefireL.list_data = g_Ceasefire.Duration;
ceasefireL.selected = g_Ceasefire.Default;
ceasefireL.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[this.selected];
updateGameAttributes();
};
}
function initVictoryConditions()
{
let victoryConditions = Engine.GetGUIObjectByName("victoryCondition");
victoryConditions.list = g_VictoryConditions.Title;
victoryConditions.list_data = g_VictoryConditions.Name;
victoryConditions.onSelectionChange = function() {
if (this.selected != -1)
{
g_GameAttributes.settings.GameType = g_VictoryConditions.Name[this.selected];
g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[this.selected];
}
updateGameAttributes();
};
victoryConditions.selected = g_VictoryConditions.Default;
}
-function initWonderDurations()
+function initVictoryDurations()
{
- let wonderConditions = Engine.GetGUIObjectByName("wonderDuration");
- wonderConditions.list = g_WonderDurations.Title;
- wonderConditions.list_data = g_WonderDurations.Duration;
- wonderConditions.onSelectionChange = function()
+ let victoryDurationsConditions = Engine.GetGUIObjectByName("victoryDuration");
+ victoryDurationsConditions.list = g_VictoryDurations.Title;
+ victoryDurationsConditions.list_data = g_VictoryDurations.Duration;
+ victoryDurationsConditions.onSelectionChange = function()
{
if (this.selected != -1)
- g_GameAttributes.settings.WonderDuration = g_WonderDurations.Duration[this.selected];
+ g_GameAttributes.settings.VictoryDuration = g_VictoryDurations.Duration[this.selected];
updateGameAttributes();
};
- wonderConditions.selected = g_WonderDurations.Default;
+ victoryDurationsConditions.selected = g_VictoryDurations.Default;
}
function initMapSizes()
{
let mapSize = Engine.GetGUIObjectByName("mapSize");
mapSize.list = g_MapSizes.Name;
mapSize.list_data = g_MapSizes.Tiles;
mapSize.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.Size = g_MapSizes.Tiles[this.selected];
updateGameAttributes();
};
mapSize.selected = 0;
}
/**
* Assign update-functions to all checkboxes.
*/
function initRadioButtons()
{
let options = {
"RevealMap": "revealMap",
"ExploreMap": "exploreMap",
"DisableTreasures": "disableTreasures",
"DisableSpies": "disableSpies",
"LockTeams": "lockTeams",
"LastManStanding" : "lastManStanding",
"CheatsEnabled": "enableCheats"
};
Object.keys(options).forEach(attribute => {
Engine.GetGUIObjectByName(options[attribute]).onPress = function() {
g_GameAttributes.settings[attribute] = this.checked;
updateGameAttributes();
};
});
Engine.GetGUIObjectByName("enableRating").onPress = function() {
g_GameAttributes.settings.RatingEnabled = this.checked;
Engine.SetRankedGame(this.checked);
Engine.GetGUIObjectByName("enableCheats").enabled = !this.checked;
Engine.GetGUIObjectByName("lockTeams").enabled = !this.checked;
updateGameAttributes();
};
Engine.GetGUIObjectByName("lockTeams").onPress = function() {
g_GameAttributes.settings.LockTeams = this.checked;
g_GameAttributes.settings.LastManStanding = false;
updateGameAttributes();
};
}
function hideStartGameButton(hidden)
{
const offset = 10;
let startGame = Engine.GetGUIObjectByName("startGame");
startGame.hidden = hidden;
let right = hidden ? startGame.size.right : startGame.size.left - offset;
let cancelGame = Engine.GetGUIObjectByName("cancelGame");
let cancelGameSize = cancelGame.size;
let xButtonSize = cancelGameSize.right - cancelGameSize.left;
cancelGameSize.right = right;
right -= xButtonSize;
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;
}
/**
* If we're a network client, hide the controls and show the text instead.
*/
function hideControls()
{
for (let ctrl of ["mapType", "mapFilter", "mapSelection", "victoryCondition", "gameSpeed", "numPlayers"])
hideControl(ctrl, ctrl + "Text");
// TODO: Shouldn't players be able to choose their own assignment?
for (let i = 0; i < g_MaxPlayers; ++i)
{
Engine.GetGUIObjectByName("playerAssignment["+i+"]").hidden = true;
Engine.GetGUIObjectByName("playerCiv["+i+"]").hidden = true;
Engine.GetGUIObjectByName("playerTeam["+i+"]").hidden = true;
}
// The start game button should be hidden until the player assignments are received
// and it is known whether the local player is an observer.
hideStartGameButton(true);
Engine.GetGUIObjectByName("startGame").enabled = true;
}
/**
* Hides the GUI controls for clients and shows the read-only label instead.
*
* @param {string} control - name of the GUI object able to change a setting
* @param {string} label - name of the GUI object displaying a setting
* @param {boolean} [allowControl] - Whether the current user is allowed to change the control.
*/
function hideControl(control, label, allowControl = g_IsController)
{
Engine.GetGUIObjectByName(control).hidden = !allowControl;
Engine.GetGUIObjectByName(label).hidden = allowControl;
}
/**
* Checks a boolean checkbox for the host and sets the text of the label for the client.
*
* @param {string} control - name of the GUI object able to change a setting
* @param {string} label - name of the GUI object displaying a setting
* @param {boolean} checked - Whether the setting is active / enabled.
*/
function setGUIBoolean(control, label, checked)
{
Engine.GetGUIObjectByName(control).checked = checked;
Engine.GetGUIObjectByName(label).caption = checked ? translate("Yes") : translate("No");
}
/**
* Hide and set some elements depending on whether we play single- or multiplayer.
*/
function initMultiplayerSettings()
{
Engine.GetGUIObjectByName("chatPanel").hidden = !g_IsNetworked;
Engine.GetGUIObjectByName("optionCheats").hidden = !g_IsNetworked;
Engine.GetGUIObjectByName("optionRating").hidden = !Engine.HasXmppClient();
Engine.GetGUIObjectByName("enableCheats").enabled = !Engine.IsRankedGame();
Engine.GetGUIObjectByName("lockTeams").enabled = !Engine.IsRankedGame();
Engine.GetGUIObjectByName("enableCheats").checked = g_GameAttributes.settings.CheatsEnabled;
Engine.GetGUIObjectByName("enableRating").checked = !!g_GameAttributes.settings.RatingEnabled;
for (let ctrl of ["enableCheats", "enableRating"])
hideControl(ctrl, ctrl + "Text");
}
/**
* Populate team-, color- and civ-dropdowns.
*/
function initPlayerAssignments()
{
let boxSpacing = 32;
for (let i = 0; i < g_MaxPlayers; ++i)
{
let box = Engine.GetGUIObjectByName("playerBox["+i+"]");
let boxSize = box.size;
let h = boxSize.bottom - boxSize.top;
boxSize.top = i * boxSpacing;
boxSize.bottom = i * boxSpacing + h;
box.size = boxSize;
let team = Engine.GetGUIObjectByName("playerTeam["+i+"]");
let teamsArray = Array(g_MaxTeams).fill(0).map((v, i) => i + 1); // 1, 2, ... MaxTeams
team.list = [translateWithContext("team", "None")].concat(teamsArray); // "None", 1, 2, ..., maxTeams
team.list_data = [-1].concat(teamsArray.map(team => team - 1)); // -1, 0, ..., (maxTeams-1)
team.selected = 0;
let playerSlot = i; // declare for inner function use
team.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.PlayerData[playerSlot].Team = this.selected - 1;
updateGameAttributes();
};
let colorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
colorPicker.list = g_PlayerColors.map(color => ' ' + '[color="' + rgbToGuiColor(color) + '"]■[/color]');
colorPicker.list_data = g_PlayerColors.map((color, index) => index);
colorPicker.selected = -1;
colorPicker.onSelectionChange = function() { selectPlayerColor(playerSlot, this.selected); };
Engine.GetGUIObjectByName("playerCiv["+i+"]").onSelectionChange = function() {
if ((this.selected != -1)&&(g_GameAttributes.mapType !== "scenario"))
g_GameAttributes.settings.PlayerData[playerSlot].Civ = this.list_data[this.selected];
updateGameAttributes();
};
}
}
/**
* Called when the client disconnects.
* The other cases from NetClient should never occur in the gamesetup.
* @param {Object} message
*/
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).
* @param {Object} message
*/
function handleReadyMessage(message)
{
--g_ReadyChanged;
if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1)
addChatMessage({
"type": "ready",
"status": message.status,
"guid": message.guid
});
if (!g_IsController)
return;
g_PlayerAssignments[message.guid].status = message.status;
updateReadyUI();
}
/**
* Called after every player is ready and the host decided to finally start the game.
* @param {Object} message
*/
function handleGamestartMessage(message)
{
// Immediately inform the lobby server instead of waiting for the load to finish
if (g_IsController && Engine.HasXmppClient())
{
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.
* @param {Object} message
*/
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);
updateGUIObjects();
}
/**
* Called whenever a client joins/leaves or any gamesetting is changed.
* @param {Object} message
*/
function handlePlayerAssignmentMessage(message)
{
for (let guid in message.newAssignments)
if (!g_PlayerAssignments[guid])
onClientJoin(guid, message.newAssignments);
for (let guid in g_PlayerAssignments)
if (!message.newAssignments[guid])
onClientLeave(guid);
g_PlayerAssignments = message.newAssignments;
hideStartGameButton(!g_IsController && g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1);
updatePlayerList();
updateReadyUI();
sendRegisterGameStanza();
}
function onClientJoin(newGUID, newAssignments)
{
addChatMessage({
"type": "connect",
"guid": newGUID,
"username": newAssignments[newGUID].name
});
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 assigned as player
if (newAssignments[newGUID].player == -1 && freeSlot == -1)
return;
// Assign the joining client to the free slot
if (g_IsController && newAssignments[newGUID].player == -1)
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;
}
/**
* Get a playersetting or return the default if it wasn't set.
*/
function getSetting(settings, defaults, property)
{
if (settings && (property in settings))
return settings[property];
if (defaults && (property in defaults))
return defaults[property];
return undefined;
}
/**
* Initialize the dropdowns containing all selectable civs (including random).
*/
function initCivNameList()
{
let civList = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => ({ "name": g_CivData[civ].Name, "code": civ })).sort(sortNameIgnoreCase);
let civListNames = [g_RandomCiv].concat(civList.map(civ => civ.name));
let civListCodes = ["random"].concat(civList.map(civ => civ.code));
for (let i = 0; i < g_MaxPlayers; ++i)
{
let civ = Engine.GetGUIObjectByName("playerCiv["+i+"]");
civ.list = civListNames;
civ.list_data = civListCodes;
civ.selected = 0;
}
}
/**
* Initialize the dropdown containing all maps for the selected maptype and mapfilter.
*/
function initMapNameList()
{
if (!g_MapPath[g_GameAttributes.mapType])
{
error("Unexpected map type: " + g_GameAttributes.mapType);
return;
}
let mapFiles = g_GameAttributes.mapType == "random" ?
getJSONFileList(g_GameAttributes.mapPath) :
getXMLFileList(g_GameAttributes.mapPath);
// Apply map filter, if any defined
// TODO: Should verify these are valid maps before adding to list
let mapList = [];
for (let mapFile of mapFiles)
{
let file = g_GameAttributes.mapPath + mapFile;
let mapData = loadMapData(file);
let mapFilter = g_MapFilters.find(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "all"));
if (!!mapData.settings && mapFilter && mapFilter.filter(mapData.settings.Keywords || []))
mapList.push({ "name": getMapDisplayName(file), "file": file });
}
translateObjectKeys(mapList, ["name"]);
mapList.sort(sortNameIgnoreCase);
let mapListNames = mapList.map(map => map.name);
let mapListFiles = mapList.map(map => map.file);
if (g_GameAttributes.mapType == "random")
{
mapListNames.unshift(g_RandomMap);
mapListFiles.unshift("random");
}
let mapSelectionBox = Engine.GetGUIObjectByName("mapSelection");
mapSelectionBox.list = mapListNames;
mapSelectionBox.list_data = mapListFiles;
mapSelectionBox.onSelectionChange = function() {
if (this.list_data[this.selected])
selectMap(this.list_data[this.selected]);
};
mapSelectionBox.selected = Math.max(0, mapListFiles.indexOf(g_GameAttributes.map || ""));
}
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 (Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true")
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
initMapNameList();
g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient();
Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled);
updateGUIObjects();
}
function savePersistMatchSettings()
{
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) => {
pData.Color = pData.Color || g_PlayerColors[index];
pData.Civ = pData.Civ || "random";
// Use default AI if the map doesn't specify any explicitly
if (!("AI" in pData))
pData.AI = g_DefaultPlayerData[index].AI;
if (!("AIDiff" in pData))
pData.AIDiff = g_DefaultPlayerData[index].AIDiff;
});
// Replace colors with the best matching color of PlayerDefaults
if (g_GameAttributes.mapType != "scenario")
{
playerData.forEach((pData, index) => {
let colorDistances = g_PlayerColors.map(color => colorDistance(color, pData.Color));
let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance)));
pData.Color = g_PlayerColors.find(color => colorDistance(color, pData.Color) == smallestDistance);
});
}
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");
}
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)
{
Engine.GetGUIObjectByName("loadingWindow").hidden = true;
Engine.GetGUIObjectByName("setupWindow").hidden = false;
initGUIObjects();
++g_LoadingState;
}
else if (g_LoadingState == 2)
{
while (true)
{
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);
}
}
updateTimers();
}
/**
* 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 ||
g_PlayerAssignments.local.player > maxPlayers)
g_PlayerAssignments = {
"local": {
"name": singleplayerName(),
"player": 1
}
};
}
/**
* Called when the host choses the number of players on a random map.
* @param {Number} num
*/
function selectNumPlayers(num)
{
if (g_IsInGuiUpdate || !g_IsController || g_GameAttributes.mapType != "random")
return;
let pData = g_GameAttributes.settings.PlayerData;
g_GameAttributes.settings.PlayerData =
num > pData.length ?
pData.concat(g_DefaultPlayerData.slice(pData.length, num)) :
pData.slice(0, num);
unassignInvalidPlayers(num);
sanitizePlayerData(g_GameAttributes.settings.PlayerData);
updateGameAttributes();
}
/**
* Assigns the given color to that player.
*/
function selectPlayerColor(playerSlot, colorIndex)
{
if (colorIndex == -1)
return;
let playerData = g_GameAttributes.settings.PlayerData;
// If someone else has that color, give that player the old color
let pData = playerData.find(pData => sameColor(g_PlayerColors[colorIndex], pData.Color));
if (pData)
pData.Color = playerData[playerSlot].Color;
// Assign the new color
playerData[playerSlot].Color = g_PlayerColors[colorIndex];
// Ensure colors are not used twice after increasing the number of players
ensureUniquePlayerColors(playerData);
if (!g_IsInGuiUpdate)
updateGameAttributes();
}
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_PlayerColors.find(color => playerData.every(pData => !sameColor(color, pData.Color)));
}
/**
* Called when the user selects a map type from the list.
*
* @param {string} type - scenario, skirmish or random
*/
function selectMapType(type)
{
if (g_IsInGuiUpdate || !g_IsController)
return;
if (!g_MapPath[type])
{
error("selectMapType: Unexpected map type " + type);
return;
}
g_MapData = {};
g_GameAttributes.map = "";
g_GameAttributes.mapType = type;
g_GameAttributes.mapPath = g_MapPath[type];
if (type != "scenario")
g_GameAttributes.settings = {
"PlayerData": g_DefaultPlayerData.slice(0, 4),
"CheatsEnabled": g_GameAttributes.settings.CheatsEnabled
};
initMapNameList();
updateGameAttributes();
}
function selectMapFilter(id)
{
if (g_IsInGuiUpdate || !g_IsController)
return;
g_GameAttributes.mapFilter = id;
initMapNameList();
updateGameAttributes();
}
function selectMap(name)
{
if (g_IsInGuiUpdate || !g_IsController || !name)
return;
// Reset some map specific properties which are not necessarily redefined on each map
for (let prop of ["TriggerScripts", "CircularMap", "Garrison"])
g_GameAttributes.settings[prop] = undefined;
let mapData = loadMapData(name);
let mapSettings = mapData && mapData.settings ? deepcopy(mapData.settings) : {};
// Reset victory conditions
if (g_GameAttributes.mapType != "random")
{
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.WonderDuration;
+ delete g_GameAttributes.settings.VictoryDuration;
delete g_GameAttributes.settings.LastManStanding;
}
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];
unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length);
updateGameAttributes();
}
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(Engine.GetGUIObjectByName("mapSelection").list_data.slice(1)));
g_GameAttributes.settings.VictoryScripts = victoryScriptsSelected;
g_GameAttributes.settings.GameType = gameTypeSelected;
}
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] == "local")
playerID = +i+1;
}
Engine.StartGame(g_GameAttributes, playerID);
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"isNetworked" : g_IsNetworked,
"playerAssignments": g_PlayerAssignments
});
}
}
/**
* 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;
let mapSettings = g_GameAttributes.settings;
// These dropdowns don't set values while g_IsInGuiUpdate
let mapName = g_GameAttributes.map || "";
let mapFilterIdx = g_MapFilters.findIndex(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "default"));
let mapTypeIdx = g_GameAttributes.mapType !== undefined ? g_MapTypes.Name.indexOf(g_GameAttributes.mapType) : g_MapTypes.Default;
let gameSpeedIdx = g_GameAttributes.gameSpeed !== undefined ? g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) : g_GameSpeeds.Default;
// These dropdowns might set the default (as they ignore g_IsInGuiUpdate)
let mapSizeIdx = mapSettings.Size !== undefined ? g_MapSizes.Tiles.indexOf(mapSettings.Size) : g_MapSizes.Default;
let victoryIdx = mapSettings.GameType !== undefined ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
- let wonderDurationIdx = mapSettings.WonderDuration !== undefined ? g_WonderDurations.Duration.indexOf(mapSettings.WonderDuration) : g_WonderDurations.Default;
+ let victoryDurationIdx = mapSettings.VictoryDuration !== undefined ? g_VictoryDurations.Duration.indexOf(mapSettings.VictoryDuration) : g_VictoryDurations.Default;
let popIdx = mapSettings.PopulationCap !== undefined ? g_PopulationCapacities.Population.indexOf(mapSettings.PopulationCap) : g_PopulationCapacities.Default;
let startingResIdx = mapSettings.StartingResources !== undefined ? g_StartingResources.Resources.indexOf(mapSettings.StartingResources) : g_StartingResources.Default;
let ceasefireIdx = mapSettings.Ceasefire !== undefined ? g_Ceasefire.Duration.indexOf(mapSettings.Ceasefire) : g_Ceasefire.Default;
let numPlayers = mapSettings.PlayerData ? mapSettings.PlayerData.length : g_MaxPlayers;
if (g_IsController)
{
Engine.GetGUIObjectByName("mapType").selected = mapTypeIdx;
Engine.GetGUIObjectByName("mapFilter").selected = mapFilterIdx;
Engine.GetGUIObjectByName("mapSelection").selected = Engine.GetGUIObjectByName("mapSelection").list_data.indexOf(mapName);
Engine.GetGUIObjectByName("mapSize").selected = mapSizeIdx;
Engine.GetGUIObjectByName("numPlayers").selected = numPlayers - 1;
Engine.GetGUIObjectByName("victoryCondition").selected = victoryIdx;
- Engine.GetGUIObjectByName("wonderDuration").selected = wonderDurationIdx;
+ Engine.GetGUIObjectByName("victoryDuration").selected = victoryDurationIdx;
Engine.GetGUIObjectByName("populationCap").selected = popIdx;
Engine.GetGUIObjectByName("gameSpeed").selected = gameSpeedIdx;
Engine.GetGUIObjectByName("ceasefire").selected = ceasefireIdx;
Engine.GetGUIObjectByName("startingResources").selected = startingResIdx;
}
else
{
Engine.GetGUIObjectByName("mapTypeText").caption = g_MapTypes.Title[mapTypeIdx];
Engine.GetGUIObjectByName("mapFilterText").caption = g_MapFilters[mapFilterIdx].name;
Engine.GetGUIObjectByName("mapSelectionText").caption = mapName == "random" ? g_RandomMap : translate(getMapDisplayName(mapName));
initMapNameList();
}
// Can be visible to both host and clients
Engine.GetGUIObjectByName("mapSizeText").caption = g_GameAttributes.mapType == "random" ? g_MapSizes.Name[mapSizeIdx] : translateWithContext("map size", "Default");
Engine.GetGUIObjectByName("numPlayersText").caption = numPlayers;
Engine.GetGUIObjectByName("victoryConditionText").caption = g_VictoryConditions.Title[victoryIdx];
- Engine.GetGUIObjectByName("wonderDurationText").caption = g_WonderDurations.Title[wonderDurationIdx];
+ Engine.GetGUIObjectByName("victoryDurationText").caption = g_VictoryDurations.Title[victoryDurationIdx];
Engine.GetGUIObjectByName("populationCapText").caption = g_PopulationCapacities.Title[popIdx];
Engine.GetGUIObjectByName("startingResourcesText").caption = g_StartingResources.Title[startingResIdx];
Engine.GetGUIObjectByName("ceasefireText").caption = g_Ceasefire.Title[ceasefireIdx];
Engine.GetGUIObjectByName("gameSpeedText").caption = g_GameSpeeds.Title[gameSpeedIdx];
setGUIBoolean("enableCheats", "enableCheatsText", !!mapSettings.CheatsEnabled);
setGUIBoolean("disableTreasures", "disableTreasuresText", !!mapSettings.DisableTreasures);
setGUIBoolean("disableSpies", "disableSpiesText", !!mapSettings.DisableSpies);
setGUIBoolean("exploreMap", "exploreMapText", !!mapSettings.ExploreMap);
setGUIBoolean("revealMap", "revealMapText", !!mapSettings.RevealMap);
setGUIBoolean("lockTeams", "lockTeamsText", !!mapSettings.LockTeams);
setGUIBoolean("lastManStanding", "lastManStandingText", !!mapSettings.LastManStanding);
setGUIBoolean("enableRating", "enableRatingText", !!mapSettings.RatingEnabled);
- Engine.GetGUIObjectByName("optionWonderDuration").hidden =
+ Engine.GetGUIObjectByName("optionVictoryDuration").hidden =
g_GameAttributes.settings.GameType &&
- g_GameAttributes.settings.GameType != "wonder";
+ g_GameAttributes.settings.GameType != "wonder" && g_GameAttributes.settings.GameType != "capture_the_relic";
Engine.GetGUIObjectByName("cheatWarningText").hidden = !g_IsNetworked || !mapSettings.CheatsEnabled;
Engine.GetGUIObjectByName("lastManStanding").enabled = !mapSettings.LockTeams;
Engine.GetGUIObjectByName("enableCheats").enabled = !mapSettings.RatingEnabled;
Engine.GetGUIObjectByName("lockTeams").enabled = !mapSettings.RatingEnabled;
// Mapsize completely hidden for non-random maps
let isRandom = g_GameAttributes.mapType == "random";
Engine.GetGUIObjectByName("mapSizeDesc").hidden = !isRandom;
Engine.GetGUIObjectByName("mapSize").hidden = !isRandom || !g_IsController;
Engine.GetGUIObjectByName("mapSizeText").hidden = !isRandom || g_IsController;
hideControl("numPlayers", "numPlayersText", isRandom && g_IsController);
let notScenario = g_GameAttributes.mapType != "scenario" && g_IsController ;
- for (let ctrl of ["victoryCondition", "wonderDuration", "populationCap",
+ for (let ctrl of ["victoryCondition", "victoryDuration", "populationCap",
"startingResources", "ceasefire", "revealMap",
"exploreMap", "disableTreasures", "disableSpies", "lockTeams", "lastManStanding"])
hideControl(ctrl, ctrl + "Text", notScenario);
Engine.GetGUIObjectByName("civResetButton").hidden = !notScenario;
Engine.GetGUIObjectByName("teamResetButton").hidden = !notScenario;
for (let i = 0; i < g_MaxPlayers; ++i)
{
Engine.GetGUIObjectByName("playerBox["+i+"]").hidden = (i >= numPlayers);
if (i >= numPlayers)
continue;
let pName = Engine.GetGUIObjectByName("playerName["+i+"]");
let pAssignment = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
let pAssignmentText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
let pCiv = Engine.GetGUIObjectByName("playerCiv["+i+"]");
let pCivText = Engine.GetGUIObjectByName("playerCivText["+i+"]");
let pTeam = Engine.GetGUIObjectByName("playerTeam["+i+"]");
let pTeamText = Engine.GetGUIObjectByName("playerTeamText["+i+"]");
let pColor = Engine.GetGUIObjectByName("playerColor["+i+"]");
let pData = mapSettings.PlayerData ? mapSettings.PlayerData[i] : {};
let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[i] : {};
let color = getSetting(pData, pDefs, "Color");
pColor.sprite = "color:" + rgbToGuiColor(color) + " 100";
pName.caption = translate(getSetting(pData, pDefs, "Name"));
let team = getSetting(pData, pDefs, "Team");
let civ = getSetting(pData, pDefs, "Civ");
pAssignmentText.caption = pAssignment.list[0] ? pAssignment.list[Math.max(0, pAssignment.selected)] : translate("Loading...");
pCivText.caption = civ == "random" ? g_RandomCiv : (g_CivData[civ] ? g_CivData[civ].Name : "Unknown");
pTeamText.caption = (team !== undefined && team >= 0) ? team+1 : "-";
pCiv.selected = civ ? pCiv.list_data.indexOf(civ) : 0;
pTeam.selected = team !== undefined && team >= 0 ? team+1 : 0;
hideControl("playerAssignment["+i+"]", "playerAssignmentText["+i+"]", g_IsController);
hideControl("playerCiv["+i+"]", "playerCivText["+i+"]", notScenario);
hideControl("playerTeam["+i+"]", "playerTeamText["+i+"]", notScenario);
// Allow host to chose player colors on non-scenario maps
let pColorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
let pColorPickerHeading = Engine.GetGUIObjectByName("playerColorHeading");
let canChangeColors = g_IsController && g_GameAttributes.mapType != "scenario";
pColorPicker.hidden = !canChangeColors;
pColorPickerHeading.hidden = !canChangeColors;
if (canChangeColors)
pColorPicker.selected = g_PlayerColors.findIndex(col => sameColor(col, color));
}
updateGameDescription();
resizeMoreOptionsWindow();
g_IsInGuiUpdate = false;
// Game attributes include AI settings, so update the player list
updatePlayerList();
resetReadyData();
// Refresh AI config page
if (g_LastViewedAIPlayer != -1)
{
Engine.PopGuiPage();
openAIConfig(g_LastViewedAIPlayer);
}
}
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();
}
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
});
}
/**
* 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;
updateGameAttributes();
}
function updatePlayerList()
{
g_IsInGuiUpdate = true;
let hostNameList = [];
let hostGuidList = [];
let assignments = [];
let aiAssignments = {};
let noAssignment;
let assignedCount = 0;
for (let guid of sortGUIDsByPlayerID())
{
let player = g_PlayerAssignments[guid].player;
if (player != -1)
hostNameList.push(g_PlayerAssignments[guid].name);
else
hostNameList.push("[color=\""+ g_UnassignedPlayerColor + "\"]" + g_PlayerAssignments[guid].name + "[/color]");
hostGuidList.push(guid);
assignments[player] = hostNameList.length-1;
if (player != -1)
++assignedCount;
}
// Only enable start button if we have enough assigned players
if (g_IsController)
Engine.GetGUIObjectByName("startGame").enabled = assignedCount > 0;
for (let ai of g_Settings.AIDescriptions)
{
// If the map uses a hidden AI then don't hide it
if (ai.data.hidden && g_GameAttributes.settings.PlayerData.every(pData => pData.AI != ai.id))
continue;
aiAssignments[ai.id] = hostNameList.length;
hostNameList.push("[color=\""+ g_AIColor + "\"]" + sprintf(translate("AI: %(ai)s"), { "ai": translate(ai.data.name) }));
hostGuidList.push("ai:" + ai.id);
}
noAssignment = hostNameList.length;
hostNameList.push("[color=\""+ g_UnassignedColor + "\"]" + translate("Unassigned"));
hostGuidList.push("");
for (let i = 0; i < g_MaxPlayers; ++i)
{
let playerSlot = i;
let playerID = i+1; // we don't show Gaia, so first slot is ID 1
let selection = assignments[playerID];
let configButton = Engine.GetGUIObjectByName("playerConfig["+i+"]");
configButton.hidden = true;
// Look for valid player slots
if (playerSlot >= g_GameAttributes.settings.PlayerData.length)
continue;
// If no human is assigned, look for an AI instead
if (selection === undefined)
{
let aiId = g_GameAttributes.settings.PlayerData[playerSlot].AI;
if (aiId)
{
// Check for a valid AI
if (aiId in aiAssignments)
{
selection = aiAssignments[aiId];
configButton.hidden = false;
configButton.onpress = function()
{
openAIConfig(playerSlot);
};
}
else
{
g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
warn("AI \"" + aiId + "\" not present. Defaulting to unassigned.");
}
}
if (!selection)
selection = noAssignment;
}
// There was a human, so make sure we don't have any AI left
// over in their slot, if we're in charge of the attributes
else if (g_IsController && g_GameAttributes.settings.PlayerData[playerSlot].AI)
{
g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
if (g_IsNetworked)
Engine.SetNetworkGameAttributes(g_GameAttributes);
}
let assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
let assignBoxText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
assignBox.list = hostNameList;
assignBox.list_data = hostGuidList;
if (assignBox.selected != selection)
assignBox.selected = selection;
assignBoxText.caption = hostNameList[selection];
if (g_IsController)
assignBox.onselectionchange = function() {
if (g_IsInGuiUpdate)
return;
let guid = hostGuidList[this.selected];
if (!guid)
{
if (g_IsNetworked)
// Unassign any host from this player slot
Engine.AssignNetworkPlayer(playerID, "");
// Remove AI from this player slot
g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
}
else if (guid.substr(0, 3) == "ai:")
{
if (g_IsNetworked)
// Unassign any host from this player slot
Engine.AssignNetworkPlayer(playerID, "");
// Set the AI for this player slot
g_GameAttributes.settings.PlayerData[playerSlot].AI = guid.substr(3);
}
else
swapPlayers(guid, playerSlot);
if (g_IsNetworked)
Engine.SetNetworkGameAttributes(g_GameAttributes);
else
updatePlayerList();
updateReadyUI();
};
}
g_IsInGuiUpdate = false;
}
function swapPlayers(guid, newSlot)
{
// Player slots are indexed from 0 as Gaia is omitted.
let newPlayerID = newSlot + 1;
let playerID = g_PlayerAssignments[guid].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;
// Swap civilizations 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];
}
}
if (g_IsNetworked)
Engine.AssignNetworkPlayer(newPlayerID, guid);
else
g_PlayerAssignments[guid].player = newPlayerID;
g_GameAttributes.settings.PlayerData[newSlot].AI = "";
}
function submitChatInput()
{
let input = Engine.GetGUIObjectByName("chatInput");
let text = input.caption;
if (!text.length)
return;
input.caption = "";
if (executeNetworkCommand(text))
return;
Engine.SendNetworkChat(text);
}
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 = "white";
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 '[color="'+ color +'"]' + 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)
notifyUser(userName, msg.text);
}
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(new Date().getTime(), translate("HH:mm"))
}),
"message": text
});
g_ChatMessages.push(text);
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}
function showMoreOptions(show)
{
Engine.GetGUIObjectByName("moreOptionsFade").hidden = !show;
Engine.GetGUIObjectByName("moreOptions").hidden = !show;
}
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);
if (g_IsController)
return;
let button = Engine.GetGUIObjectByName("startGame");
button.caption = g_ReadyData[g_IsReady].caption;
button.tooltip = g_ReadyData[g_IsReady].tooltip;
}
function updateReadyUI()
{
if (!g_IsNetworked)
return;
let isAI = new Array(g_MaxPlayers + 1).fill(true);
let allReady = true;
for (let guid in g_PlayerAssignments)
{
// We don't really care whether observers are ready.
if (g_PlayerAssignments[guid].player == -1 || !g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
continue;
let pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1] : {};
let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[g_PlayerAssignments[guid].player - 1] : {};
isAI[g_PlayerAssignments[guid].player] = false;
if (g_PlayerAssignments[guid].status)
Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption =
'[color="' + g_ReadyData[+g_PlayerAssignments[guid].status].color + '"]' +
translate(getSetting(pData, pDefs, "Name")) + '[/color]';
else
{
Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption = translate(getSetting(pData, pDefs, "Name"));
allReady = false;
}
}
// AIs are always ready.
for (let playerid = 0; playerid < g_MaxPlayers; ++playerid)
{
if (!g_GameAttributes.settings.PlayerData[playerid])
continue;
let pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[playerid] : {};
let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[playerid] : {};
if (isAI[playerid + 1])
Engine.GetGUIObjectByName("playerName[" + playerid + "]").caption =
'[color="' + g_ReadyData[2].color + '"]' + translate(getSetting(pData, pDefs, "Name")) + '[/color]';
}
// The host is not allowed to start until everyone is ready.
if (g_IsNetworked && g_IsController)
{
let startGameButton = Engine.GetGUIObjectByName("startGame");
startGameButton.enabled = allReady;
// Add a explanation on to the tooltip if disabled.
let disabledIndex = startGameButton.tooltip.indexOf('Disabled');
if (disabledIndex != -1 && allReady)
startGameButton.tooltip = startGameButton.tooltip.substring(0, disabledIndex - 2);
else if (disabledIndex == -1 && !allReady)
startGameButton.tooltip = startGameButton.tooltip + " (Disabled until all players are ready)";
}
}
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.
*/
function sendRegisterGameStanza()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
let selectedMapSize = Engine.GetGUIObjectByName("mapSize").selected;
let selectedVictoryCondition = Engine.GetGUIObjectByName("victoryCondition").selected;
let mapSize = g_GameAttributes.mapType == "random" ? Engine.GetGUIObjectByName("mapSize").list_data[selectedMapSize] : "Default";
let victoryCondition = Engine.GetGUIObjectByName("victoryCondition").list[selectedVictoryCondition];
let clients = formatClientsForStanza();
let stanza = {
"name": g_ServerName,
"port": g_ServerPort,
"mapName": g_GameAttributes.map,
"niceMapName": getMapDisplayName(g_GameAttributes.map),
"mapSize": mapSize,
"mapType": g_GameAttributes.mapType,
"victoryCondition": victoryCondition,
"nbp": clients.connectedPlayers,
"maxnbp": g_GameAttributes.settings.PlayerData.length,
"players": clients.list,
};
// 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);
}
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.xml (revision 19344)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.xml (revision 19345)
@@ -1,477 +1,477 @@
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 19344)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 19345)
@@ -1,1565 +1,1565 @@
const g_IsReplay = Engine.IsVisualReplay();
const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
const g_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly || g_IsReplay));
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_WonderDurations = prepareForDropdown(g_Settings && g_Settings.WonderDurations);
+const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
/**
* Colors to flash when pop limit reached.
*/
const g_DefaultPopulationColor = "white";
const g_PopulationAlertColor = "orange";
/**
* A random file will be played. TODO: more variety
*/
const g_Ambient = [ "audio/ambient/dayscape/day_temperate_gen_03.ogg" ];
/**
* Map, player and match settings set in gamesetup.
*/
const g_GameAttributes = Object.freeze(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 lastTickTime = new Date();
/**
* Not constant as we add "gaia".
*/
var g_CivData = {};
/**
* 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 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 simulation state (updated on every simulation update).
*/
var g_SimState;
var g_EntityStates = {};
var g_TemplateData = {};
var g_TemplateDataWithoutLocalization = {};
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 heroes.
*/
var g_Heroes = [];
/**
* 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"];
/**
* Cache the idle worker status.
*/
var g_HasIdleWorker = false;
function GetSimState()
{
if (!g_SimState)
g_SimState = Engine.GuiInterfaceCall("GetSimulationState");
return g_SimState;
}
function GetEntityState(entId)
{
if (!g_EntityStates[entId])
g_EntityStates[entId] = Engine.GuiInterfaceCall("GetEntityState", entId);
return g_EntityStates[entId];
}
function GetExtendedEntityState(entId)
{
let entState = GetEntityState(entId);
if (!entState || entState.extended)
return entState;
let extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId);
for (let prop in extension)
entState[prop] = extension[prop];
entState.extended = true;
g_EntityStates[entId] = entState;
return entState;
}
function GetTemplateData(templateName)
{
if (!(templateName in g_TemplateData))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
translateObjectKeys(template, ["specific", "generic", "tooltip"]);
g_TemplateData[templateName] = template;
}
return g_TemplateData[templateName];
}
function GetTemplateDataWithoutLocalization(templateName)
{
if (!(templateName in g_TemplateDataWithoutLocalization))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
g_TemplateDataWithoutLocalization[templateName] = template;
}
return g_TemplateDataWithoutLocalization[templateName];
}
function GetTechnologyData(technologyName, civ)
{
if (!g_TechnologyData[civ])
g_TechnologyData[civ] = {};
if (!(technologyName in g_TechnologyData[civ]))
{
let template = Engine.GuiInterfaceCall("GetTechnologyData", { "name": technologyName, "civ": civ });
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
g_TechnologyData[civ][technologyName] = 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 // Needed for autostart loading option
{
if (g_IsReplay)
g_PlayerAssignments.local.player = -1;
}
updatePlayerData();
g_CivData = loadCivData();
g_CivData.gaia = { "Code": "gaia", "Name": translate("Gaia") };
initializeMusic(); // before changing the perspective
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.list = g_GameSpeeds.Title;
gameSpeed.list_data = g_GameSpeeds.Speed;
let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(Engine.GetSimRate());
gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default;
gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); };
initMenuPosition();
resizeDiplomacyDialog();
resizeTradeDialog();
for (let slot in Engine.GetGUIObjectByName("unitHeroPanel").children)
initGUIHeroes(slot);
// Populate player selection dropdown
let playerNames = [translate("Observer")];
let playerIDs = [-1];
for (let player in g_Players)
{
playerIDs.push(player);
playerNames.push(colorizePlayernameHelper("■", player) + " " + g_Players[player].name);
}
// Select "observer" item when rejoining as a defeated player
let viewedPlayer = g_Players[Engine.GetPlayerID()];
let viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer");
viewPlayerDropdown.list = playerNames;
viewPlayerDropdown.list_data = playerIDs;
viewPlayerDropdown.selected = viewedPlayer && viewedPlayer.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;
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;
}
/**
* 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("tradeHelp").tooltip = colorizeHotkey(
translate("Select one type of goods you want to modify by clicking on it (Pressing %(hotkey)s while selecting will also bring its share to 100%%) and then use the arrows of the other types to modify their shares."),
"session.fulltradeswap");
}
function initGUIHeroes(slot)
{
let button = Engine.GetGUIObjectByName("unitHeroButton[" + slot + "]");
button.onPress = function() {
let hero = g_Heroes.find(hero => hero.slot !== undefined && hero.slot == slot);
if (!hero)
return;
if (!Engine.HotkeyIsPressed("selection.add"))
g_Selection.reset();
g_Selection.addList([hero.ent]);
};
button.onDoublePress = function() {
let hero = g_Heroes.find(hero => hero.slot !== undefined && hero.slot == slot);
if (hero)
selectAndMoveTo(getEntityOrHolder(hero.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)
global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music);
global.music.setState(global.music.states.PEACE);
playAmbient();
}
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);
updateTopPanel();
updateChatAddressees();
updateHotkeyTooltips();
// Update GUI and clear player-dependent cache
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 a player has won or was defeated.
*/
function playerFinished(player, won)
{
if (player == Engine.GetPlayerID())
reportGame();
updatePlayerData();
updateChatAddressees();
if (player != g_ViewedPlayer)
return;
// Select "observer" item on loss. On win enable observermode without changing perspective
Engine.GetGUIObjectByName("viewPlayer").selected = won ? g_ViewedPlayer + 1 : 0;
if (player != Engine.GetPlayerID() || 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 - Structure Tree"), {
"civ": g_CivData[g_Players[g_ViewedPlayer].civ].Name
});
}
Engine.GetGUIObjectByName("optionFollowPlayer").hidden = !g_IsObserver || !isPlayer;
let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
viewPlayer.hidden = !g_IsObserver && !g_DevSettings.changePerspective;
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%+20 0 100%-226 100%" : "200 0 100%-475 100%";
Engine.GetGUIObjectByName("pauseButton").enabled = !g_IsObserver || !g_IsNetworked;
Engine.GetGUIObjectByName("menuResignButton").enabled = !g_IsObserver;
}
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": "defeat-player",
"playerId": Engine.GetPlayerID(),
"resign": true
});
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();
Engine.EndGame();
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_summary.xml", {
"sim": simData,
"gui": {
"assignedPlayer": Engine.GetPlayerID(),
"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 = new Date();
let tickLength = new Date() - lastTickTime;
lastTickTime = now;
handleNetMessages();
updateCursorAndTooltip();
if (g_Selection.dirty)
{
g_Selection.dirty = false;
updateGUIObjects();
// Display rally points for selected buildings
if (Engine.GetPlayerID() != -1)
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
updateTimers();
updateMenuPosition(tickLength);
// When training is blocked, flash population (alternates color every 500msec)
Engine.GetGUIObjectByName("resourcePop").textcolor = g_IsTrainingBlocked && Date.now() % 1000 < 500 ? g_PopulationAlertColor : g_DefaultPopulationColor;
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function changeGameSpeed(speed)
{
if (!g_IsNetworked)
Engine.SetSimRate(speed);
}
function hasIdleWorker()
{
return Engine.GuiInterfaceCall("HasIdleUnits", {
"viewedPlayer": g_ViewedPlayer,
"idleClasses": g_WorkerTypes,
"excludeUnits": []
});
}
function updateIdleWorkerButton()
{
g_HasIdleWorker = hasIdleWorker();
let idleWorkerButton = Engine.GetGUIObjectByName("idleOverlay");
let prefix = "stretched:session/";
if (!g_HasIdleWorker)
idleWorkerButton.sprite = prefix + "minimap-idle-disabled.png";
else if (idleWorkerButton.sprite != prefix + "minimap-idle-highlight.png")
idleWorkerButton.sprite = prefix + "minimap-idle.png";
}
function onSimulationUpdate()
{
g_EntityStates = {};
g_TemplateData = {};
g_TechnologyData = {};
g_SimState = Engine.GuiInterfaceCall("GetSimulationState");
if (!g_SimState)
return;
handleNotifications();
updateGUIObjects();
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 updateGUIObjects()
{
g_Selection.update();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updateHeroes();
displayHeroes();
updateGroups();
updateDebug();
updatePlayerDisplay();
updateResearchDisplay();
updateSelectionDetails();
updateBuildingPlacementPreview();
updateTimeNotifications();
updateIdleWorkerButton();
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]);
}
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 updateHeroes()
{
let playerState = GetSimState().players[g_ViewedPlayer];
let heroes = playerState ? playerState.heroes : [];
g_Heroes = g_Heroes.filter(hero => heroes.find(ent => ent == hero.ent));
for (let ent of heroes)
{
let heroState = GetExtendedEntityState(ent);
let template = GetTemplateData(heroState.template);
let hero = g_Heroes.find(hero => ent == hero.ent);
if (!hero)
{
hero = {
"ent": ent,
"tooltip": undefined,
"sprite": "stretched:session/portraits/" + template.icon,
"maxHitpoints": undefined,
"currentHitpoints": heroState.hitpoints,
"previousHitpoints": undefined
};
g_Heroes.push(hero);
}
hero.tooltip = createHeroTooltip(heroState, template);
hero.previousHitpoints = hero.currentHitpoints;
hero.currentHitpoints = heroState.hitpoints;
hero.maxHitpoints = heroState.maxHitpoints;
}
}
function createHeroTooltip(heroState, template)
{
return [
"[font=\"sans-bold-16\"]" + template.name.specific + "[/font]" + "\n" +
sprintf(translate("%(label)s %(current)s / %(max)s"), {
"label": "[font=\"sans-bold-13\"]" + translate("Health:") + "[/font]",
"current": Math.ceil(heroState.hitpoints),
"max": Math.ceil(heroState.maxHitpoints)
}),
getAttackTooltip(heroState),
getArmorTooltip(heroState),
getEntityTooltip(heroState)
].filter(tip => tip).join("\n");
}
function displayHeroes()
{
let buttons = Engine.GetGUIObjectByName("unitHeroPanel").children;
buttons.forEach((button, slot) => {
if (button.hidden || g_Heroes.some(hero => hero.slot !== undefined && hero.slot == slot))
return;
button.hidden = true;
stopColorFade("heroHitOverlay[" + slot + "]");
});
// The slot identifies the button, displayIndex determines its position.
for (let displayIndex = 0; displayIndex < Math.min(g_Heroes.length, buttons.length); ++displayIndex)
{
let hero = g_Heroes[displayIndex];
// Find the first unused slot if new, otherwise reuse previous.
let slot = hero.slot === undefined ?
buttons.findIndex(button => button.hidden) :
hero.slot;
let heroButton = Engine.GetGUIObjectByName("unitHeroButton[" + slot + "]");
heroButton.tooltip = hero.tooltip;
updateGUIStatusBar("heroHealthBar[" + slot + "]", hero.currentHitpoints, hero.maxHitpoints);
if (hero.slot === undefined)
{
let heroImage = Engine.GetGUIObjectByName("unitHeroImage[" + slot + "]");
heroImage.sprite = hero.sprite;
heroButton.hidden = false;
hero.slot = slot;
}
// If the health of the hero changed since the last update, trigger the animation.
if (hero.previousHitpoints > hero.currentHitpoints)
startColorFade("heroHitOverlay[" + slot + "]", 100, 0,
colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit);
// TODO: Instead of instant position changes, animate button movement.
setPanelObjectPosition(heroButton, 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 = deepcopy(GetSimState());
conciseSimState.players = "<<>>";
let text = "simulation: " + uneval(conciseSimState);
let selection = g_Selection.toList();
if (selection.length)
{
let entState = GetExtendedEntityState(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, "\\[");
}
function getAllyStatTooltip(resource)
{
let playersState = GetSimState().players;
let ret = "";
for (let player in playersState)
{
if (player != 0 &&
player != g_ViewedPlayer &&
g_Players[player].state != "defeated" &&
(g_IsObserver ||
playersState[g_ViewedPlayer].hasSharedLos &&
g_Players[player].isMutualAlly[g_ViewedPlayer]))
{
ret += "\n" + sprintf(translate("%(playername)s: %(statValue)s"),{
"playername": colorizePlayernameHelper("■", player) + " " + g_Players[player].name,
"statValue": resource == "pop" ?
sprintf(translate("%(popCount)s/%(popLimit)s/%(popMax)s"), playersState[player]) :
Math.round(playersState[player].resourceCounts[resource])
});
}
}
return ret;
}
function updatePlayerDisplay()
{
let playerState = GetSimState().players[g_ViewedPlayer];
if (!playerState)
return;
let resCodes = g_ResourceData.GetCodes();
let resNames = g_ResourceData.GetNames();
for (let r = 0; r < resCodes.length; ++r)
{
if (!Engine.GetGUIObjectByName("resource["+r+"]"))
break;
let res = resCodes[r];
Engine.GetGUIObjectByName("resource["+r+"]").tooltip = getLocalizedResourceName(resNames[res], "firstWord") + getAllyStatTooltip(res);
Engine.GetGUIObjectByName("resource["+r+"]_count").caption = Math.floor(playerState.resourceCounts[res]);
}
Engine.GetGUIObjectByName("resourcePop").caption = sprintf(translate("%(popCount)s/%(popLimit)s"), playerState);
Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" +
sprintf(translate("Maximum population: %(popCap)s"), { "popCap": playerState.popMax }) +
getAllyStatTooltip("pop");
g_IsTrainingBlocked = playerState.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);
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;
++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
});
}
// 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 };
if (g_GameAttributes.settings.LockTeams)
minPData.Team = pData.Team;
if (pData.AI)
{
minPData.AI = pData.AI;
minPData.AIDiff = pData.AIDiff;
}
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 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 = "";
playerStatistics.tradeIncome = "";
// Tribute
playerStatistics.tributesSent = "";
playerStatistics.tributesReceived = "";
// Total
playerStatistics.economyScore = "";
playerStatistics.militaryScore = "";
playerStatistics.totalScore = "";
// Various
playerStatistics.treasuresCollected = "";
playerStatistics.lootCollected = "";
playerStatistics.feminisation = "";
playerStatistics.percentMapExplored = "";
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];
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.statistics[resourcesCounterType][resourcesType] + ",";
playerStatistics.resourcesGathered.vegetarianFood += player.statistics.resourcesGathered.vegetarianFood + ",";
for (let unitCounterType of unitsCountersTypes)
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] += player.statistics[unitCounterType][unitsClass] + ",";
for (let buildingCounterType of buildingsCountersTypes)
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] += player.statistics[buildingCounterType][buildingsClass] + ",";
let total = 0;
for (let type in player.statistics.resourcesGathered)
total += player.statistics.resourcesGathered[type];
playerStatistics.economyScore += total + ",";
playerStatistics.militaryScore += Math.round((player.statistics.enemyUnitsKilledValue +
player.statistics.enemyBuildingsDestroyedValue) / 10) + ",";
playerStatistics.totalScore += (total + Math.round((player.statistics.enemyUnitsKilledValue +
player.statistics.enemyBuildingsDestroyedValue) / 10)) + ",";
playerStatistics.tradeIncome += player.statistics.tradeIncome + ",";
playerStatistics.tributesSent += player.statistics.tributesSent + ",";
playerStatistics.tributesReceived += player.statistics.tributesReceived + ",";
playerStatistics.percentMapExplored += player.statistics.percentMapExplored + ",";
playerStatistics.treasuresCollected += player.statistics.treasuresCollected + ",";
playerStatistics.lootCollected += player.statistics.lootCollected + ",";
}
// 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];
}
reportObject.tributesSent = playerStatistics.tributesSent;
reportObject.tributesReceived = playerStatistics.tributesReceived;
reportObject.percentMapExplored = playerStatistics.percentMapExplored;
reportObject.treasuresCollected = playerStatistics.treasuresCollected;
reportObject.lootCollected = playerStatistics.lootCollected;
reportObject.tradeIncome = playerStatistics.tradeIncome;
Engine.SendGameReport(reportObject);
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (revision 19345)
@@ -0,0 +1,180 @@
+let g_CatafalqueTemplate = "other/special_catafalque";
+
+Trigger.prototype.InitCaptureTheRelic = function()
+{
+ // Attempt to spawn relics using gaia entities in neutral territory
+ // If there are none, try to spawn using gaia entities in non-neutral territory
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ let cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
+ let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager);
+ let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+
+ let potentialGaiaSpawnPoints = [];
+
+ let potentialSpawnPoints = cmpRangeManager.GetEntitiesByPlayer(0).filter(entity => {
+ let cmpPosition = Engine.QueryInterface(entity, IID_Position);
+ if (!cmpPosition || !cmpPosition.IsInWorld())
+ return false;
+
+ let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
+ if (!cmpIdentity)
+ return false;
+
+ let templateName = cmpTemplateManager.GetCurrentTemplateName(entity);
+ if (!templateName)
+ return false;
+
+ let template = cmpTemplateManager.GetTemplate(templateName);
+ if (!template || template.UnitMotionFlying)
+ return false;
+
+ let pos = cmpPosition.GetPosition();
+ if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z))
+ return false;
+
+ if (cmpTerritoryManager.GetOwner(pos.x, pos.z) == 0)
+ potentialGaiaSpawnPoints.push(entity);
+
+ return true;
+ });
+
+ if (potentialGaiaSpawnPoints.length)
+ potentialSpawnPoints = potentialGaiaSpawnPoints;
+
+ let numSpawnedRelics = Math.ceil(TriggerHelper.GetNumberOfPlayers() / 2);
+ 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), g_CatafalqueTemplate, 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(randFloat(0, 2 * Math.PI));
+ }
+};
+
+Trigger.prototype.CheckCaptureTheRelicVictory = function(data)
+{
+ let cmpIdentity = Engine.QueryInterface(data.entity, IID_Identity);
+ if (!cmpIdentity || !cmpIdentity.HasClass("Relic") || data.from == -1)
+ return;
+
+ if (data.to == -1)
+ {
+ error("Relic entity " + data.entity + " has been destroyed");
+ return;
+ }
+
+ --this.playerRelicsCount[data.from];
+ ++this.playerRelicsCount[data.to];
+
+ this.CheckCaptureTheRelicCountdown();
+};
+
+/**
+ * Check if an individual player or team has acquired all relics.
+ * Also check if the countdown needs to be stopped if a player/team no longer has all relics.
+ */
+Trigger.prototype.CheckCaptureTheRelicCountdown = function()
+{
+ let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
+
+ for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
+ {
+ let playerAndAllies = cmpEndGameManager.GetAlliedVictory() ?
+ QueryPlayerIDInterface(playerID).GetMutualAllies() : [playerID];
+
+ let teamRelicsOwned = 0;
+
+ for (let ally of playerAndAllies)
+ teamRelicsOwned += this.playerRelicsCount[ally];
+
+ if (teamRelicsOwned == this.relics.length)
+ {
+ this.StartCaptureTheRelicCountdown(playerAndAllies);
+ return;
+ }
+ }
+
+ this.DeleteCaptureTheRelicVictoryMessages();
+};
+
+Trigger.prototype.DeleteCaptureTheRelicVictoryMessages = function()
+{
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.relicsVictoryTimer);
+
+ let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ cmpGuiInterface.DeleteTimeNotification(this.ownRelicsVictoryMessage);
+ cmpGuiInterface.DeleteTimeNotification(this.othersRelicsVictoryMessage);
+};
+
+Trigger.prototype.StartCaptureTheRelicCountdown = function(playerAndAllies)
+{
+ 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);
+ }
+
+ let others = [-1];
+ for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
+ {
+ let cmpPlayer = QueryPlayerIDInterface(playerID);
+ if (cmpPlayer.GetState() == "won")
+ return;
+
+ if (playerAndAllies.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().victoryDuration || 0;
+
+ let isTeam = playerAndAllies.length > 1;
+ this.ownRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({
+ "message": isTeam ?
+ markForTranslation("%(player)s's team has captured all relics and will have won in %(time)s") :
+ markForTranslation("%(player)s has captured all relics and will have won in %(time)s"),
+ "players": others,
+ "parameters": {
+ "player": cmpPlayer.GetName()
+ },
+ "translateMessage": true,
+ "translateParameters": []
+ }, captureTheRelicDuration);
+
+ this.othersRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({
+ "message": isTeam ?
+ markForTranslation("Your team has captured all relics and will have won in %(time)s") :
+ markForTranslation("You have captured all relics and will have won in %(time)s"),
+ "players": playerAndAllies,
+ "translateMessage": true
+ }, captureTheRelicDuration);
+
+ this.relicsVictoryTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_EndGameManager,
+ "MarkPlayerAsWon", captureTheRelicDuration, playerAndAllies[0]);
+};
+
+{
+ let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+ cmpTrigger.relics = [];
+ cmpTrigger.playerRelicsCount = [];
+ cmpTrigger.relicsVictoryTimer = undefined;
+ cmpTrigger.ownRelicsVictoryMessage = undefined;
+ cmpTrigger.othersRelicsVictoryMessage = undefined;
+
+ cmpTrigger.DoAfterDelay(0, "InitCaptureTheRelic", {});
+ cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "CheckCaptureTheRelicCountdown", { "enabled": true });
+ cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckCaptureTheRelicVictory", { "enabled": true });
+ cmpTrigger.RegisterTrigger("OnPlayerWon", "DeleteCaptureTheRelicVictoryMessages", { "enabled": true });
+}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js (revision 19344)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js (revision 19345)
@@ -1,83 +1,83 @@
Trigger.prototype.CheckWonderVictory = function(data)
{
let ent = data.entity;
let cmpWonder = Engine.QueryInterface(ent, IID_Wonder);
if (!cmpWonder)
return;
let timer = this.wonderVictoryTimers[ent];
let messages = this.wonderVictoryMessages[ent] || {};
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
if (timer)
{
cmpTimer.CancelTimer(timer);
cmpGuiInterface.DeleteTimeNotification(messages.ownMessage);
cmpGuiInterface.DeleteTimeNotification(messages.otherMessage);
}
if (data.to <= 0)
return;
// Create new messages, and start timer to register defeat.
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
// Add -1 to notify observers too
let players = [-1];
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
if (cmpPlayer.GetState() == "won")
return;
if (i != data.to)
players.push(i);
}
let cmpPlayer = QueryOwnerInterface(ent, IID_Player);
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
- let wonderDuration = cmpEndGameManager.GetGameTypeSettings().wonderDuration || 0;
+ let wonderDuration = cmpEndGameManager.GetGameTypeSettings().victoryDuration || 0;
messages.otherMessage = cmpGuiInterface.AddTimeNotification({
"message": markForTranslation("%(_player_)s will have won in %(time)s"),
"players": players,
"parameters": {
"_player_": cmpPlayer.GetPlayerID()
},
"translateMessage": true,
"translateParameters": [],
}, wonderDuration);
messages.ownMessage = cmpGuiInterface.AddTimeNotification({
"message": markForTranslation("You will have won in %(time)s"),
"players": [data.to],
"translateMessage": true,
}, wonderDuration);
timer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_EndGameManager,
"MarkPlayerAsWon", wonderDuration, data.to);
this.wonderVictoryTimers[ent] = timer;
this.wonderVictoryMessages[ent] = messages;
};
Trigger.prototype.DeleteWonderVictoryMessages = function(data)
{
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
for (let ent in this.wonderVictoryMessages)
{
cmpGuiInterface.DeleteTimeNotification(this.wonderVictoryMessages[ent].ownMessage);
cmpGuiInterface.DeleteTimeNotification(this.wonderVictoryMessages[ent].otherMessage);
cmpTimer.CancelTimer(this.wonderVictoryTimers[ent]);
}
};
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckWonderVictory", { "enabled": true });
cmpTrigger.RegisterTrigger("OnPlayerWon", "DeleteWonderVictoryMessages", { "enabled": true });
cmpTrigger.wonderVictoryTimers = {};
cmpTrigger.wonderVictoryMessages = {};
Index: ps/trunk/binaries/data/mods/public/simulation/components/Identity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 19344)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 19345)
@@ -1,166 +1,166 @@
function Identity() {}
Identity.prototype.Schema =
"Specifies various names and values associated with the unit type, typically for GUI display to users." +
"" +
"athen" +
"Athenian Hoplite" +
"Hoplī́tēs Athēnaïkós" +
"units/athen_infantry_spearman.png" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"Basic" +
"Advanced" +
"Elite" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
- "" +
+ "" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Identity.prototype.Init = function()
{
// caching
this.classesList = GetIdentityClasses(this.template);
this.visibleClassesList = GetVisibleIdentityClasses(this.template);
};
Identity.prototype.Deserialize = function ()
{
this.Init();
};
Identity.prototype.Serialize = null; // we have no dynamic state to save
Identity.prototype.GetCiv = function()
{
return this.template.Civ;
};
Identity.prototype.GetLang = function()
{
return this.template.Lang || "greek"; // ugly default
};
Identity.prototype.GetGender = function()
{
return this.template.Gender || "male"; // ugly default
};
Identity.prototype.GetRank = function()
{
return (this.template.Rank || "");
};
Identity.prototype.GetClassesList = function()
{
return this.classesList;
};
Identity.prototype.GetVisibleClassesList = function()
{
return this.visibleClassesList;
};
Identity.prototype.HasClass = function(name)
{
return this.GetClassesList().indexOf(name) != -1;
};
Identity.prototype.GetFormationsList = function()
{
if (this.template.Formations && "_string" in this.template.Formations)
{
var string = this.template.Formations._string;
return string.split(/\s+/);
}
return [];
};
Identity.prototype.CanUseFormation = function(template)
{
return this.GetFormationsList().indexOf(template) != -1;
};
Identity.prototype.GetSelectionGroupName = function()
{
return (this.template.SelectionGroupName || "");
};
Identity.prototype.GetGenericName = function()
{
return this.template.GenericName;
};
Engine.RegisterComponentType(IID_Identity, "Identity", Identity);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Trigger.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Trigger.js (revision 19344)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Trigger.js (revision 19345)
@@ -1,302 +1,308 @@
function Trigger() {}
Trigger.prototype.Schema =
"";
/**
* Events we're able to receive and call handlers for.
*/
Trigger.prototype.eventNames =
[
"CinemaPathEnded",
"CinemaQueueEnded",
"ConstructionStarted",
+ "DiplomacyChanged",
"InitGame",
"Interval",
"OwnershipChanged",
"PlayerCommand",
"PlayerDefeated",
"PlayerWon",
"Range",
"ResearchFinished",
"ResearchQueued",
"StructureBuilt",
"TrainingFinished",
"TrainingQueued",
"TreasureCollected"
];
Trigger.prototype.Init = function()
{
this.triggerPoints = {};
// Each event has its own set of actions determined by the map maker.
for (let eventName of this.eventNames)
this["On" + eventName + "Actions"] = {};
};
Trigger.prototype.RegisterTriggerPoint = function(ref, ent)
{
if (!this.triggerPoints[ref])
this.triggerPoints[ref] = [];
this.triggerPoints[ref].push(ent);
};
Trigger.prototype.RemoveRegisteredTriggerPoint = function(ref, ent)
{
if (!this.triggerPoints[ref])
{
warn("no trigger points found with ref "+ref);
return;
}
let i = this.triggerPoints[ref].indexOf(ent);
if (i == -1)
{
warn("entity " + ent + " wasn't found under the trigger points with ref "+ref);
return;
}
this.triggerPoints[ref].splice(i, 1);
};
Trigger.prototype.GetTriggerPoints = function(ref)
{
return this.triggerPoints[ref] || [];
};
/**
* Binds a function to the specified event.
*
* @param {string} event - One of eventNames
* @param {string} action - Name of a function available to this object
* @param {Object} data - f.e. enabled or not, delay for timers, range for range triggers
*
* @example
* data = { enabled: true, interval: 1000, delay: 500 }
*
* Range trigger:
* data.entities = [id1, id2] * Ids of the source
* data.players = [1,2,3,...] * list of player ids
* data.minRange = 0 * Minimum range for the query
* data.maxRange = -1 * Maximum range for the query (-1 = no maximum)
* data.requiredComponent = 0 * Required component id the entities will have
* data.enabled = false * If the query is enabled by default
*/
Trigger.prototype.RegisterTrigger = function(event, action, data)
{
let eventString = event + "Actions";
if (!this[eventString])
{
warn("Trigger.js: Invalid trigger event \"" + event + "\".");
return;
}
if (this[eventString][action])
{
warn("Trigger.js: Trigger \"" + action + "\" has been registered before. Aborting...");
return;
}
// clone the data to be sure it's only modified locally
// We could run into triggers overwriting each other's data otherwise.
// F.e. getting the wrong timer tag
data = clone(data) || { "enabled": false };
this[eventString][action] = data;
// setup range query
if (event == "OnRange")
{
if (!data.entities)
{
warn("Trigger.js: Range triggers should carry extra data");
return;
}
data.queries = [];
for (let ent of data.entities)
{
let cmpTriggerPoint = Engine.QueryInterface(ent, IID_TriggerPoint);
if (!cmpTriggerPoint)
{
warn("Trigger.js: Range triggers must be defined on trigger points");
continue;
}
data.queries.push(cmpTriggerPoint.RegisterRangeTrigger(action, data));
}
}
if (data.enabled)
this.EnableTrigger(event, action);
};
Trigger.prototype.DisableTrigger = function(event, action)
{
let eventString = event + "Actions";
if (!this[eventString][action])
{
warn("Trigger.js: Disabling unknown trigger");
return;
}
let data = this[eventString][action];
// special casing interval and range triggers for performance
if (event == "OnInterval")
{
if (!data.timer) // don't disable it a second time
return;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(data.timer);
data.timer = null;
}
else if (event == "OnRange")
{
if (!data.queries)
{
warn("Trigger.js: Range query wasn't set up before trying to disable it.");
return;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let query of data.queries)
cmpRangeManager.DisableActiveQuery(query);
}
data.enabled = false;
};
Trigger.prototype.EnableTrigger = function(event, action)
{
let eventString = event + "Actions";
if (!this[eventString][action])
{
warn("Trigger.js: Enabling unknown trigger");
return;
}
let data = this[eventString][action];
// special casing interval and range triggers for performance
if (event == "OnInterval")
{
if (data.timer) // don't enable it a second time
return;
if (!data.interval)
{
warn("Trigger.js: An interval trigger should have an intervel in its data");
return;
}
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
data.timer = cmpTimer.SetInterval(this.entity, IID_Trigger, "DoAction",
data.delay || 0, data.interval, { "action" : action });
}
else if (event == "OnRange")
{
if (!data.queries)
{
warn("Trigger.js: Range query wasn't set up before");
return;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let query of data.queries)
cmpRangeManager.EnableActiveQuery(query);
}
data.enabled = true;
};
/**
* This function executes the actions bound to the events.
* It's either called directlty from other simulation scripts,
* or from message listeners in this file
*
* @param {string} event - One of eventNames
* @param {Object} data - will be passed to the actions
*/
Trigger.prototype.CallEvent = function(event, data)
{
let eventString = "On" + event + "Actions";
if (!this[eventString])
{
warn("Trigger.js: Unknown trigger event called:\"" + event + "\".");
return;
}
for (let action in this[eventString])
if (this[eventString][action].enabled)
this.DoAction({ "action": action, "data":data });
};
Trigger.prototype.OnGlobalInitGame = function(msg)
{
this.CallEvent("InitGame", {});
};
Trigger.prototype.OnGlobalConstructionFinished = function(msg)
{
this.CallEvent("StructureBuilt", { "building": msg.newentity });
};
Trigger.prototype.OnGlobalTrainingFinished = function(msg)
{
this.CallEvent("TrainingFinished", msg);
// The data for this one is {"entities": createdEnts,
// "owner": cmpOwnership.GetOwner(),
// "metadata": metadata}
// See function "SpawnUnits" in ProductionQueue for more details
};
Trigger.prototype.OnGlobalResearchFinished = function(msg)
{
this.CallEvent("ResearchFinished", msg);
// The data for this one is { "player": playerID, "tech": tech }
};
Trigger.prototype.OnGlobalCinemaPathEnded = function(msg)
{
this.CallEvent("CinemaPathEnded", msg);
};
Trigger.prototype.OnGlobalCinemaQueueEnded = function(msg)
{
this.CallEvent("CinemaQueueEnded", msg);
};
Trigger.prototype.OnGlobalOwnershipChanged = function(msg)
{
this.CallEvent("OwnershipChanged", msg);
// data is {"entity": ent, "from": playerId, "to": playerId}
};
Trigger.prototype.OnGlobalPlayerDefeated = function(msg)
{
this.CallEvent("PlayerDefeated", msg);
};
Trigger.prototype.OnGlobalPlayerWon = function(msg)
{
this.CallEvent("PlayerWon", msg);
};
+Trigger.prototype.OnGlobalDiplomacyChanged = function(msg)
+{
+ this.CallEvent("DiplomacyChanged", msg);
+};
+
/**
* Execute a function after a certain delay.
*
* @param {Number} time - delay in milleseconds
* @param {String} action - Name of the action function
* @param {Object} data - will be passed to the action function
*/
Trigger.prototype.DoAfterDelay = function(miliseconds, action, data)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
return cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "DoAction", miliseconds, {
"action": action,
"data": data
});
};
/**
* Called by the trigger listeners to exucute the actual action. Including sanity checks.
*/
Trigger.prototype.DoAction = function(msg)
{
if (this[msg.action])
this[msg.action](msg.data || null);
else
warn("Trigger.js: called a trigger action '" + msg.action + "' that wasn't found");
};
Engine.RegisterSystemComponentType(IID_Trigger, "Trigger", Trigger);
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 19344)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EndGameManager.js (revision 19345)
@@ -1,29 +1,29 @@
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("EndGameManager.js");
let cmpEndGameManager = ConstructComponent(SYSTEM_ENTITY, "EndGameManager");
let playerEnt1 = 1;
-let wonderDuration = 2 * 60 * 1000;
+let victoryDuration = 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.SetGameType("wonder", { "victoryDuration": victoryDuration });
TS_ASSERT_EQUALS(cmpEndGameManager.skipAlliedVictoryCheck, false);
TS_ASSERT(cmpEndGameManager.GetGameType() == "wonder");
-TS_ASSERT_EQUALS(cmpEndGameManager.GetGameTypeSettings().wonderDuration, wonderDuration);
+TS_ASSERT_EQUALS(cmpEndGameManager.GetGameTypeSettings().victoryDuration, victoryDuration);
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 (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/capture_the_relic.json (revision 19345)
@@ -0,0 +1,15 @@
+{
+ "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"
+ ]
+ }
+}
Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_times.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_times.json (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_times.json (revision 19345)
@@ -0,0 +1,4 @@
+{
+ "Times": [0, 1, 3, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 75, 90, 105, 120],
+ "Default": 20
+}
Property changes on: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_times.json
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js (revision 19344)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js (revision 19345)
@@ -1,72 +1,72 @@
/**
* 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);
}
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
let gameTypeSettings = {};
- if (settings.WonderDuration)
- gameTypeSettings.wonderDuration = settings.WonderDuration * 60 * 1000;
+ if (settings.VictoryDuration)
+ gameTypeSettings.victoryDuration = settings.VictoryDuration * 60 * 1000;
if (settings.GameType)
cmpEndGameManager.SetGameType(settings.GameType, gameTypeSettings);
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/binaries/data/mods/public/simulation/templates/other/special_catafalque.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/other/special_catafalque.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/other/special_catafalque.xml (revision 19345)
@@ -0,0 +1,53 @@
+
+
+
+ 250
+ 0
+ 10
+
+
+ 0
+
+
+ 2.0
+
+
+
+ true
+
+
+ gaia
+ -ConquestCritical
+
+ Catafalque
+ units/catafalque.png
+ A catafalque that holds the remains of a great leader.
+ Relic
+
+
+
+
+
+
+
+ actor/singlesteps/steps_grass.xml
+ actor/singlesteps/steps_grass.xml
+
+ actor/singlesteps/steps_grass.xml
+
+
+
+ standground
+ false
+
+
+ large
+
+ 8
+
+ 5
+
+
+ units/global/catafalque.xml
+
+
Index: ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp
===================================================================
--- ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp (revision 19344)
+++ ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp (revision 19345)
@@ -1,577 +1,578 @@
-/* Copyright (C) 2016 Wildfire Games.
+/* Copyright (C) 2017 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"
enum
{
ID_MapName,
ID_MapDescription,
ID_MapReveal,
ID_MapType,
ID_MapPreview,
ID_MapTeams,
ID_MapKW_Demo,
ID_MapKW_Naval,
ID_RandomScript,
ID_RandomSize,
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 OnEdit(wxCommandEvent& WXUNUSED(evt))
{
SendToEngine();
}
std::set m_MapSettingsKeywords;
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());
gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Reveal map")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
gridSizer->Add(Tooltipped(new wxCheckBox(this, ID_MapReveal, wxEmptyString),
_("If checked, players won't need to explore")));
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());
gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Lock teams")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
gridSizer->Add(Tooltipped(new wxCheckBox(this, ID_MapTeams, wxEmptyString),
_("If checked, teams will be locked")));
sizer->Add(gridSizer, wxSizerFlags().Expand());
sizer->AddSpacer(5);
wxStaticBoxSizer* keywordsSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Keywords"));
wxFlexGridSizer* kwGridSizer = new wxFlexGridSizer(4, 5, 5);
kwGridSizer->Add(new wxStaticText(this, wxID_ANY, _("Demo")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
kwGridSizer->Add(Tooltipped(new wxCheckBox(this, ID_MapKW_Demo, wxEmptyString),
_("If checked, map will only be visible using filters in game setup")));
kwGridSizer->Add(new wxStaticText(this, wxID_ANY, _("Naval")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
kwGridSizer->Add(Tooltipped(new wxCheckBox(this, ID_MapKW_Naval, wxEmptyString),
_("If checked, map will only be visible using filters in game setup")));
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);
// 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);
}
}
void MapSettingsControl::SetMapSettings(const AtObj& obj)
{
m_MapSettings = obj;
m_MapSettings.NotifyObservers();
SendToEngine();
}
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());
// 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());
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)
{
m_MapSettingsCtrl = new MapSettingsControl(this, m_ScenarioEditor);
m_MainSizer->Add(m_MapSettingsCtrl, wxSizerFlags().Expand());
{
/////////////////////////////////////////////////////////////////////////
// Random map settings
wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Random map"));
sizer->Add(new wxChoice(this, ID_RandomScript), wxSizerFlags().Expand());
sizer->AddSpacer(5);
sizer->Add(new wxButton(this, ID_OpenPlayerPanel, _T("Change players")), wxSizerFlags().Expand());
sizer->AddSpacer(5);
wxFlexGridSizer* gridSizer = new wxFlexGridSizer(2, 5, 5);
gridSizer->AddGrowableCol(1);
wxChoice* sizeChoice = new wxChoice(this, ID_RandomSize);
gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Map size")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
gridSizer->Add(sizeChoice, wxSizerFlags().Expand());
gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Random seed")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
wxBoxSizer* seedSizer = new wxBoxSizer(wxHORIZONTAL);
seedSizer->Add(Tooltipped(new wxTextCtrl(this, ID_RandomSeed, _T("0"), wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_NUMERIC)),
_("Seed value for random map")), wxSizerFlags(1).Expand());
seedSizer->Add(Tooltipped(new wxButton(this, ID_RandomReseed, _("R"), wxDefaultPosition, wxSize(24, -1)),
_("New random seed")));
gridSizer->Add(seedSizer, wxSizerFlags().Expand());
sizer->Add(gridSizer, wxSizerFlags().Expand());
sizer->AddSpacer(5);
sizer->Add(Tooltipped(new wxButton(this, ID_RandomGenerate, _("Generate map")),
_("Run selected random map script")), wxSizerFlags().Expand());
m_MainSizer->Add(sizer, wxSizerFlags().Expand().Border(wxTOP, 10));
}
{
/////////////////////////////////////////////////////////////////////////
// Simulation buttons
wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Simulation test"));
wxGridSizer* gridSizer = new wxGridSizer(5);
gridSizer->Add(Tooltipped(new wxButton(this, ID_SimPlay, _("Play")),
_("Run the simulation at normal speed")), wxSizerFlags().Expand());
gridSizer->Add(Tooltipped(new wxButton(this, ID_SimFast, _("Fast")),
_("Run the simulation at 8x speed")), wxSizerFlags().Expand());
gridSizer->Add(Tooltipped(new wxButton(this, ID_SimSlow, _("Slow")),
_("Run the simulation at 1/8x speed")), wxSizerFlags().Expand());
gridSizer->Add(Tooltipped(new wxButton(this, ID_SimPause, _("Pause")),
_("Pause the simulation")), wxSizerFlags().Expand());
gridSizer->Add(Tooltipped(new wxButton(this, ID_SimReset, _("Reset")),
_("Reset the editor to initial state")), wxSizerFlags().Expand());
sizer->Add(gridSizer, wxSizerFlags().Expand());
UpdateSimButtons();
m_MainSizer->Add(sizer, wxSizerFlags().Expand().Border(wxTOP, 10));
}
}
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"]);
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.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();