Index: ps/trunk/binaries/data/mods/public/gui/manual/manual.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/manual/manual.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/manual/manual.js (nonexistent)
@@ -1,15 +0,0 @@
-var hasCallback = false;
-
-function init(data)
-{
- Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile("gui/manual/intro.txt"));
- hasCallback = data && data.callback;
-}
-
-function closeManual()
-{
- if (hasCallback)
- Engine.PopGuiPageCB();
- else
- Engine.PopGuiPage();
-}
Property changes on: ps/trunk/binaries/data/mods/public/gui/manual/manual.js
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/splashscreen/splashscreen.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/splashscreen/splashscreen.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/splashscreen/splashscreen.js (revision 22676)
@@ -1,14 +1,14 @@
var g_SplashScreenFile = "gui/splashscreen/splashscreen.txt";
function init(data)
{
Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile(g_SplashScreenFile));
Engine.GetGUIObjectByName("displaySplashScreen").checked = Engine.ConfigDB_GetValue("user", "gui.splashscreen.enable") === "true";
}
function closePage()
{
Engine.ConfigDB_CreateAndWriteValueToFile("user", "gui.splashscreen.enable", String(Engine.GetGUIObjectByName("displaySplashScreen").checked), "config/user.cfg");
Engine.ConfigDB_CreateAndWriteValueToFile("user", "gui.splashscreen.version", Engine.GetFileMTime(g_SplashScreenFile), "config/user.cfg");
- Engine.PopGuiPageCB();
+ Engine.PopGuiPage();
}
Index: ps/trunk/source/gui/GUIManager.cpp
===================================================================
--- ps/trunk/source/gui/GUIManager.cpp (revision 22675)
+++ ps/trunk/source/gui/GUIManager.cpp (revision 22676)
@@ -1,419 +1,423 @@
/* Copyright (C) 2019 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 "GUIManager.h"
#include "gui/CGUI.h"
#include "lib/timer.h"
#include "ps/Filesystem.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include "ps/XML/Xeromyces.h"
#include "ps/GameSetup/Config.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRuntime.h"
CGUIManager* g_GUI = NULL;
// General TODOs:
//
// A lot of the CGUI data could (and should) be shared between
// multiple pages, instead of treating them as completely independent, to save
// memory and loading time.
// called from main loop when (input) events are received.
// event is passed to other handlers if false is returned.
// trampoline: we don't want to make the HandleEvent implementation static
InReaction gui_handler(const SDL_Event_* ev)
{
PROFILE("GUI event handler");
return g_GUI->HandleEvent(ev);
}
static Status ReloadChangedFileCB(void* param, const VfsPath& path)
{
return static_cast(param)->ReloadChangedFile(path);
}
CGUIManager::CGUIManager()
{
m_ScriptRuntime = g_ScriptRuntime;
m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIManager", m_ScriptRuntime));
m_ScriptInterface->SetCallbackData(this);
m_ScriptInterface->LoadGlobalScripts();
if (!CXeromyces::AddValidator(g_VFS, "gui_page", "gui/gui_page.rng"))
LOGERROR("CGUIManager: failed to load GUI page grammar file 'gui/gui_page.rng'");
if (!CXeromyces::AddValidator(g_VFS, "gui", "gui/gui.rng"))
LOGERROR("CGUIManager: failed to load GUI XML grammar file 'gui/gui.rng'");
RegisterFileReloadFunc(ReloadChangedFileCB, this);
}
CGUIManager::~CGUIManager()
{
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
}
bool CGUIManager::HasPages()
{
return !m_PageStack.empty();
}
void CGUIManager::SwitchPage(const CStrW& pageName, ScriptInterface* srcScriptInterface, JS::HandleValue initData)
{
// The page stack is cleared (including the script context where initData came from),
// therefore we have to clone initData.
shared_ptr initDataClone;
if (!initData.isUndefined())
initDataClone = srcScriptInterface->WriteStructuredClone(initData);
m_PageStack.clear();
- PushPage(pageName, initDataClone);
+ PushPage(pageName, initDataClone, JS::UndefinedHandleValue);
}
-void CGUIManager::PushPage(const CStrW& pageName, shared_ptr initData)
+void CGUIManager::PushPage(const CStrW& pageName, shared_ptr initData, JS::HandleValue callbackFunction)
{
+ // Store the callback handler in the current GUI page before opening the new one
+ if (!m_PageStack.empty() && !callbackFunction.isUndefined())
+ m_PageStack.back().SetCallbackFunction(*m_ScriptInterface, callbackFunction);
+
// Push the page prior to loading its contents, because that may push
// another GUI page on init which should be pushed on top of this new page.
m_PageStack.emplace_back(pageName, initData);
m_PageStack.back().LoadPage(m_ScriptRuntime);
+
ResetCursor();
}
-void CGUIManager::PopPage()
+void CGUIManager::PopPage(shared_ptr args)
{
if (m_PageStack.size() < 2)
{
debug_warn(L"Tried to pop GUI page when there's < 2 in the stack");
return;
}
m_PageStack.pop_back();
-}
-
-void CGUIManager::PopPageCB(shared_ptr args)
-{
- shared_ptr initDataClone = m_PageStack.back().initData;
- PopPage();
-
- shared_ptr scriptInterface = m_PageStack.back().gui->GetScriptInterface();
- JSContext* cx = scriptInterface->GetContext();
- JSAutoRequest rq(cx);
-
- JS::RootedValue initDataVal(cx);
- if (!initDataClone)
- {
- LOGERROR("Called PopPageCB when initData (which should contain the callback function name) isn't set!");
- return;
- }
-
- scriptInterface->ReadStructuredClone(initDataClone, &initDataVal);
-
- if (!scriptInterface->HasProperty(initDataVal, "callback"))
- {
- LOGERROR("Called PopPageCB when the callback function name isn't set!");
- return;
- }
-
- std::string callback;
- if (!scriptInterface->GetProperty(initDataVal, "callback", callback))
- {
- LOGERROR("Failed to get the callback property as a string from initData in PopPageCB!");
- return;
- }
-
- JS::RootedValue global(cx, scriptInterface->GetGlobalObject());
- if (!scriptInterface->HasProperty(global, callback.c_str()))
- {
- LOGERROR("The specified callback function %s does not exist in the page %s", callback, utf8_from_wstring(m_PageStack.back().name));
- return;
- }
-
- JS::RootedValue argVal(cx);
- if (args)
- scriptInterface->ReadStructuredClone(args, &argVal);
- if (!scriptInterface->CallFunctionVoid(global, callback.c_str(), argVal))
- {
- LOGERROR("Failed to call the callback function %s in the page %s", callback, utf8_from_wstring(m_PageStack.back().name));
- return;
- }
+ m_PageStack.back().PerformCallbackFunction(args);
}
CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const shared_ptr initData)
- : name(pageName), initData(initData), inputs(), gui()
+ : name(pageName), initData(initData), inputs(), gui(), callbackFunction()
{
}
void CGUIManager::SGUIPage::LoadPage(shared_ptr scriptRuntime)
{
// If we're hotloading then try to grab some data from the previous page
shared_ptr hotloadData;
if (gui)
{
shared_ptr scriptInterface = gui->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, scriptInterface->GetGlobalObject());
JS::RootedValue hotloadDataVal(cx);
scriptInterface->CallFunction(global, "getHotloadData", &hotloadDataVal);
hotloadData = scriptInterface->WriteStructuredClone(hotloadDataVal);
}
inputs.clear();
gui.reset(new CGUI(scriptRuntime));
gui->Initialize();
VfsPath path = VfsPath("gui") / name;
inputs.insert(path);
CXeromyces xero;
if (xero.Load(g_VFS, path, "gui_page") != PSRETURN_OK)
// Fail silently (Xeromyces reported the error)
return;
int elmt_page = xero.GetElementID("page");
int elmt_include = xero.GetElementID("include");
XMBElement root = xero.GetRoot();
if (root.GetNodeName() != elmt_page)
{
LOGERROR("GUI page '%s' must have root element ", utf8_from_wstring(name));
return;
}
XERO_ITER_EL(root, node)
{
if (node.GetNodeName() != elmt_include)
{
LOGERROR("GUI page '%s' must only have elements inside ", utf8_from_wstring(name));
continue;
}
std::string name = node.GetText();
CStrW nameW (node.GetText().FromUTF8());
PROFILE2("load gui xml");
PROFILE2_ATTR("name: %s", name.c_str());
TIMER(nameW.c_str());
if (name.back() == '/')
{
VfsPath directory = VfsPath("gui") / nameW;
VfsPaths pathnames;
vfs::GetPathnames(g_VFS, directory, L"*.xml", pathnames);
for (const VfsPath& path : pathnames)
gui->LoadXmlFile(path, inputs);
}
else
{
VfsPath path = VfsPath("gui") / nameW;
gui->LoadXmlFile(path, inputs);
}
}
gui->SendEventToAll("load");
shared_ptr scriptInterface = gui->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue initDataVal(cx);
JS::RootedValue hotloadDataVal(cx);
JS::RootedValue global(cx, scriptInterface->GetGlobalObject());
if (initData)
scriptInterface->ReadStructuredClone(initData, &initDataVal);
if (hotloadData)
scriptInterface->ReadStructuredClone(hotloadData, &hotloadDataVal);
if (scriptInterface->HasProperty(global, "init") &&
!scriptInterface->CallFunctionVoid(global, "init", initDataVal, hotloadDataVal))
LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(name));
}
+void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc)
+{
+ if (!callbackFunc.isObject())
+ {
+ LOGERROR("Given callback handler is not an object!");
+ return;
+ }
+
+ // Does not require JSAutoRequest
+ if (!JS_ObjectIsFunction(scriptInterface.GetContext(), &callbackFunc.toObject()))
+ {
+ LOGERROR("Given callback handler is not a function!");
+ return;
+ }
+
+ callbackFunction = std::make_shared(scriptInterface.GetJSRuntime(), callbackFunc);
+}
+
+void CGUIManager::SGUIPage::PerformCallbackFunction(shared_ptr args)
+{
+ if (!callbackFunction)
+ return;
+
+ shared_ptr scriptInterface = gui->GetScriptInterface();
+ JSContext* cx = scriptInterface->GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedObject globalObj(cx, &scriptInterface->GetGlobalObject().toObject());
+
+ JS::RootedValue funcVal(cx, *callbackFunction);
+
+ // Delete the callback function, so that it is not called again
+ callbackFunction.reset();
+
+ JS::RootedValue argVal(cx);
+ if (args)
+ scriptInterface->ReadStructuredClone(args, &argVal);
+
+ JS::AutoValueVector paramData(cx);
+ paramData.append(argVal);
+
+ JS::RootedValue result(cx);
+
+ JS_CallFunctionValue(cx, globalObj, funcVal, paramData, &result);
+}
+
Status CGUIManager::ReloadChangedFile(const VfsPath& path)
{
for (SGUIPage& p : m_PageStack)
if (p.inputs.count(path))
{
LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.name));
p.LoadPage(m_ScriptRuntime);
// TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators
}
return INFO::OK;
}
Status CGUIManager::ReloadAllPages()
{
// TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators
for (SGUIPage& p : m_PageStack)
p.LoadPage(m_ScriptRuntime);
return INFO::OK;
}
void CGUIManager::ResetCursor()
{
g_CursorName = g_DefaultCursor;
}
std::string CGUIManager::GetSavedGameData()
{
shared_ptr scriptInterface = top()->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue data(cx);
JS::RootedValue global(cx, top()->GetGlobalObject());
scriptInterface->CallFunction(global, "getSavedGameData", &data);
return scriptInterface->StringifyJSON(&data, false);
}
void CGUIManager::RestoreSavedGameData(const std::string& jsonData)
{
shared_ptr scriptInterface = top()->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, top()->GetGlobalObject());
JS::RootedValue dataVal(cx);
scriptInterface->ParseJSON(jsonData, &dataVal);
scriptInterface->CallFunctionVoid(global, "restoreSavedGameData", dataVal);
}
InReaction CGUIManager::HandleEvent(const SDL_Event_* ev)
{
// We want scripts to have access to the raw input events, so they can do complex
// processing when necessary (e.g. for unit selection and camera movement).
// Sometimes they'll want to be layered behind the GUI widgets (e.g. to detect mousedowns on the
// visible game area), sometimes they'll want to intercepts events before the GUI (e.g.
// to capture all mouse events until a mouseup after dragging).
// So we call two separate handler functions:
bool handled;
{
PROFILE("handleInputBeforeGui");
JSContext* cx = top()->GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, top()->GetGlobalObject());
if (top()->GetScriptInterface()->CallFunction(global, "handleInputBeforeGui", handled, *ev, top()->FindObjectUnderMouse()))
if (handled)
return IN_HANDLED;
}
{
PROFILE("handle event in native GUI");
InReaction r = top()->HandleEvent(ev);
if (r != IN_PASS)
return r;
}
{
// We can't take the following lines out of this scope because top() may be another gui page than it was when calling handleInputBeforeGui!
JSContext* cx = top()->GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, top()->GetGlobalObject());
PROFILE("handleInputAfterGui");
if (top()->GetScriptInterface()->CallFunction(global, "handleInputAfterGui", handled, *ev))
if (handled)
return IN_HANDLED;
}
return IN_PASS;
}
void CGUIManager::SendEventToAll(const CStr& eventName) const
{
top()->SendEventToAll(eventName);
}
void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const
{
top()->SendEventToAll(eventName, paramData);
}
void CGUIManager::TickObjects()
{
PROFILE3("gui tick");
// We share the script runtime with everything else that runs in the same thread.
// This call makes sure we trigger GC regularly even if the simulation is not running.
m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(1.0f);
// Save an immutable copy so iterators aren't invalidated by tick handlers
PageStackType pageStack = m_PageStack;
for (const SGUIPage& p : pageStack)
p.gui->TickObjects();
}
void CGUIManager::Draw()
{
PROFILE3_GPU("gui");
for (const SGUIPage& p : m_PageStack)
p.gui->Draw();
}
void CGUIManager::UpdateResolution()
{
// Save an immutable copy so iterators aren't invalidated by event handlers
PageStackType pageStack = m_PageStack;
for (const SGUIPage& p : pageStack)
{
p.gui->UpdateResolution();
p.gui->SendEventToAll("WindowResized");
}
}
bool CGUIManager::TemplateExists(const std::string& templateName) const
{
return m_TemplateLoader.TemplateExists(templateName);
}
const CParamNode& CGUIManager::GetTemplate(const std::string& templateName)
{
const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity");
if (!templateRoot.IsOk())
LOGERROR("Invalid template found for '%s'", templateName.c_str());
return templateRoot;
}
// This returns a shared_ptr to make sure the CGUI doesn't get deallocated
// while we're in the middle of calling a function on it (e.g. if a GUI script
// calls SwitchPage)
shared_ptr CGUIManager::top() const
{
ENSURE(m_PageStack.size());
return m_PageStack.back().gui;
}
Index: ps/trunk/source/gui/scripting/JSInterface_GUIManager.cpp
===================================================================
--- ps/trunk/source/gui/scripting/JSInterface_GUIManager.cpp (revision 22675)
+++ ps/trunk/source/gui/scripting/JSInterface_GUIManager.cpp (revision 22676)
@@ -1,94 +1,88 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* Copyright (C) 2019 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 "JSInterface_GUIManager.h"
#include "gui/CGUI.h"
#include "gui/GUIManager.h"
#include "gui/IGUIObject.h"
#include "ps/GameSetup/Config.h"
#include "scriptinterface/ScriptInterface.h"
// Note that the initData argument may only contain clonable data.
// Functions aren't supported for example!
-void JSI_GUIManager::PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData)
+void JSI_GUIManager::PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction)
{
- g_GUI->PushPage(name, pCxPrivate->pScriptInterface->WriteStructuredClone(initData));
+ g_GUI->PushPage(name, pCxPrivate->pScriptInterface->WriteStructuredClone(initData), callbackFunction);
}
void JSI_GUIManager::SwitchGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData)
{
g_GUI->SwitchPage(name, pCxPrivate->pScriptInterface, initData);
}
-void JSI_GUIManager::PopGuiPage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
+void JSI_GUIManager::PopGuiPage(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args)
{
- g_GUI->PopPage();
-}
-
-void JSI_GUIManager::PopGuiPageCB(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args)
-{
- g_GUI->PopPageCB(pCxPrivate->pScriptInterface->WriteStructuredClone(args));
+ g_GUI->PopPage(pCxPrivate->pScriptInterface->WriteStructuredClone(args));
}
JS::Value JSI_GUIManager::GetGUIObjectByName(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name)
{
CGUI* guiPage = static_cast(pCxPrivate->pCBData);
IGUIObject* guiObj = guiPage->FindObjectByName(name);
if (!guiObj)
return JS::UndefinedValue();
return JS::ObjectValue(*guiObj->GetJSObject());
}
std::wstring JSI_GUIManager::SetCursor(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
{
std::wstring old = g_CursorName;
g_CursorName = name;
return old;
}
void JSI_GUIManager::ResetCursor(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_GUI->ResetCursor();
}
bool JSI_GUIManager::TemplateExists(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName)
{
return g_GUI->TemplateExists(templateName);
}
CParamNode JSI_GUIManager::GetTemplate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName)
{
return g_GUI->GetTemplate(templateName);
}
void JSI_GUIManager::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
- scriptInterface.RegisterFunction("PushGuiPage");
+ scriptInterface.RegisterFunction("PushGuiPage");
scriptInterface.RegisterFunction("SwitchGuiPage");
- scriptInterface.RegisterFunction("PopGuiPage");
- scriptInterface.RegisterFunction("PopGuiPageCB");
+ scriptInterface.RegisterFunction("PopGuiPage");
scriptInterface.RegisterFunction("GetGUIObjectByName");
scriptInterface.RegisterFunction("SetCursor");
scriptInterface.RegisterFunction("ResetCursor");
scriptInterface.RegisterFunction("TemplateExists");
scriptInterface.RegisterFunction("GetTemplate");
}
Index: ps/trunk/binaries/data/mods/mod/gui/common/functions_msgbox.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/common/functions_msgbox.js (revision 22675)
+++ ps/trunk/binaries/data/mods/mod/gui/common/functions_msgbox.js (revision 22676)
@@ -1,61 +1,29 @@
-// We want to pass callback functions for the different buttons in a convenient way.
-// Because passing functions accross compartment boundaries is a pain, we just store them here together with some optional arguments.
-// The messageBox page will return the code of the pressed button and the according function will be called.
-var g_MessageBoxBtnFunctions = [];
-var g_MessageBoxCallbackArgs = [];
-
-function messageBoxCallbackFunction(btnCode)
-{
- if (btnCode !== undefined && g_MessageBoxBtnFunctions[btnCode])
- {
- // Cache the variables to make it possible to call a messageBox from a callback function.
- let callbackFunction = g_MessageBoxBtnFunctions[btnCode];
- let callbackArgs = g_MessageBoxCallbackArgs[btnCode];
-
- g_MessageBoxBtnFunctions = [];
- g_MessageBoxCallbackArgs = [];
-
- if (callbackArgs !== undefined)
- callbackFunction(callbackArgs);
- else
- callbackFunction();
- return;
- }
-
- g_MessageBoxBtnFunctions = [];
- g_MessageBoxCallbackArgs = [];
-}
-
function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, mbCallbackArgs)
{
- if (g_MessageBoxBtnFunctions && g_MessageBoxBtnFunctions.length)
- {
- warn("A messagebox was called when a previous callback function is still set, aborting!");
- return;
- }
-
- g_MessageBoxBtnFunctions = mbBtnCode;
- g_MessageBoxCallbackArgs = mbCallbackArgs || g_MessageBoxCallbackArgs;
-
- Engine.PushGuiPage("page_msgbox.xml", {
- "width": mbWidth,
- "height": mbHeight,
- "message": mbMessage,
- "title": mbTitle,
- "buttonCaptions": mbButtonCaptions,
- "callback": mbBtnCode && "messageBoxCallbackFunction"
- });
+ Engine.PushGuiPage(
+ "page_msgbox.xml",
+ {
+ "width": mbWidth,
+ "height": mbHeight,
+ "message": mbMessage,
+ "title": mbTitle,
+ "buttonCaptions": mbButtonCaptions
+ },
+ btnCode => {
+ if (mbBtnCode !== undefined && mbBtnCode[btnCode])
+ mbBtnCode[btnCode](mbCallbackArgs ? mbCallbackArgs[btnCode] : undefined);
+ });
}
function openURL(url)
{
Engine.OpenURL(url);
messageBox(
600, 200,
sprintf(
translate("Opening %(url)s\n in default web browser. Please wait…"),
{ "url": url }
),
translate("Opening page"));
}
Index: ps/trunk/binaries/data/mods/mod/gui/common/terms.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/common/terms.js (revision 22675)
+++ ps/trunk/binaries/data/mods/mod/gui/common/terms.js (revision 22676)
@@ -1,50 +1,55 @@
var g_Terms = {};
function initTerms(terms)
{
g_Terms = terms;
}
function openTerms(page)
{
- Engine.PushGuiPage("page_termsdialog.xml", {
- "file": g_Terms[page].file,
- "title": g_Terms[page].title,
- "sprintf": g_Terms[page].sprintf,
- "urlButtons": g_Terms[page].urlButtons || [],
- "termsURL": g_Terms[page].termsURL || undefined,
- "page": page,
- "callback": "acceptTerms"
- });
-}
-
-function acceptTerms(data)
-{
- g_Terms[data.page].accepted = data.accepted;
- Engine.ConfigDB_CreateAndWriteValueToFile("user", g_Terms[data.page].config, data.accepted ? getTermsHash(data.page) : "0", "config/user.cfg");
-
- if (g_Terms[data.page].callback)
- g_Terms[data.page].callback(data);
+ Engine.PushGuiPage(
+ "page_termsdialog.xml",
+ {
+ "file": g_Terms[page].file,
+ "title": g_Terms[page].title,
+ "sprintf": g_Terms[page].sprintf,
+ "urlButtons": g_Terms[page].urlButtons || [],
+ "termsURL": g_Terms[page].termsURL || undefined,
+ "page": page
+ },
+ data => {
+ g_Terms[data.page].accepted = data.accepted;
+
+ Engine.ConfigDB_CreateAndWriteValueToFile(
+ "user",
+ g_Terms[data.page].config,
+ data.accepted ? getTermsHash(data.page) : "0",
+ "config/user.cfg");
+
+ if (g_Terms[data.page].callback)
+ g_Terms[data.page].callback(data);
+ }
+ );
}
function checkTerms()
{
for (let page in g_Terms)
if (!g_Terms[page].accepted)
return g_Terms[page].instruction || page;
return "";
}
function getTermsHash(page)
{
return Engine.CalculateMD5(
(g_Terms[page].salt ? g_Terms[page].salt() : "") +
Engine.ReadFile(g_Terms[page].file));
}
function loadTermsAcceptance()
{
for (let page in g_Terms)
g_Terms[page].accepted = Engine.ConfigDB_GetValue("user", g_Terms[page].config) == getTermsHash(page);
}
Index: ps/trunk/binaries/data/mods/mod/gui/modio/modio.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modio/modio.js (revision 22675)
+++ ps/trunk/binaries/data/mods/mod/gui/modio/modio.js (revision 22676)
@@ -1,344 +1,344 @@
var g_ModsAvailableOnline = [];
/**
* Indicates if we have encountered an error in one of the network-interaction attempts.
*
* We use a global so we don't get multiple messageBoxes appearing (one for each "tick").
*
* Set to `true` by showErrorMessageBox
* Set to `false` by init, updateModList, downloadFile, and cancelRequest
*/
var g_Failure;
/**
* Indicates if the user has cancelled a request.
*
* Primarily used so the user can cancel the mod list fetch, as whenever that get cancelled,
* the modio state reverts to "ready", even if we've successfully listed mods before.
*
* Set to `true` by cancelRequest
* Set to `false` by updateModList, and downloadFile
*/
var g_RequestCancelled;
var g_RequestStartTime;
/**
* Returns true if ModIoAdvanceRequest should be called.
*/
var g_ModIOState = {
/**
* Finished status indicators
*/
"ready": progressData => {
// GameID acquired, ready to fetch mod list
if (!g_RequestCancelled)
updateModList();
return true;
},
"listed": progressData => {
// List of available mods acquired
// Only run this once (for each update).
if (Engine.GetGUIObjectByName("modsAvailableList").list.length)
return true;
hideDialog();
Engine.GetGUIObjectByName("refreshButton").enabled = true;
g_ModsAvailableOnline = Engine.ModIoGetMods();
displayMods();
return true;
},
"success": progressData => {
// Successfully acquired a mod file
hideDialog();
Engine.GetGUIObjectByName("downloadButton").enabled = true;
return true;
},
/**
* In-progress status indicators.
*/
"gameid": progressData => {
// Acquiring GameID from mod.io
return true;
},
"listing": progressData => {
// Acquiring list of available mods from mod.io
return true;
},
"downloading": progressData => {
// Downloading a mod file
updateProgressBar(progressData.progress, g_ModsAvailableOnline[selectedModIndex()].filesize);
return true;
},
/**
* Error/Failure status indicators.
*/
"failed_gameid": progressData => {
// Game ID couldn't be retrieved
showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "Game ID could not be retrieved.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Initialization Error"),
[translate("Abort"), translate("Retry")],
[closePage, init]);
return false;
},
"failed_listing": progressData => {
// Mod list couldn't be retrieved
showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "Mod List could not be retrieved.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Fetch Error"),
[translate("Abort"), translate("Retry")],
[cancelModListUpdate, updateModList]);
return false;
},
"failed_downloading": progressData => {
// File couldn't be retrieved
showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "File download failed.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Download Error"),
[translate("Abort"), translate("Retry")],
[cancelRequest, downloadMod]);
return false;
},
"failed_filecheck": progressData => {
// The file is corrupted
showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "File verification error.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Verification Error"),
[translate("Abort")],
[cancelRequest]);
return false;
},
/**
* Default
*/
"none": progressData => {
// Nothing has happened yet.
return true;
}
};
function init(data)
{
progressDialog(
translate("Initializing mod.io interface."),
translate("Initializing"),
false,
translate("Cancel"),
closePage);
g_Failure = false;
Engine.ModIoStartGetGameId();
}
function onTick()
{
let progressData = Engine.ModIoGetDownloadProgress();
let handler = g_ModIOState[progressData.status];
if (!handler)
{
warn("Unrecognized progress status: " + progressData.status);
return;
}
if (handler(progressData))
Engine.ModIoAdvanceRequest();
}
function displayMods()
{
let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
let selectedMod = modsAvailableList.list[modsAvailableList.selected];
modsAvailableList.selected = -1;
let displayedMods = clone(g_ModsAvailableOnline);
for (let i = 0; i < displayedMods.length; ++i)
displayedMods[i].i = i;
let filterColumns = ["name", "name_id", "summary"];
let filterText = Engine.GetGUIObjectByName("modFilter").caption.toLowerCase();
displayedMods = displayedMods.filter(mod => filterColumns.some(column => mod[column].toLowerCase().indexOf(filterText) != -1));
displayedMods.sort((mod1, mod2) =>
modsAvailableList.selected_column_order *
(modsAvailableList.selected_column == "filesize" ?
mod1.filesize - mod2.filesize :
String(mod1[modsAvailableList.selected_column]).localeCompare(String(mod2[modsAvailableList.selected_column]))));
modsAvailableList.list_name = displayedMods.map(mod => mod.name);
modsAvailableList.list_name_id = displayedMods.map(mod => mod.name_id);
modsAvailableList.list_version = displayedMods.map(mod => mod.version);
modsAvailableList.list_filesize = displayedMods.map(mod => filesizeToString(mod.filesize));
modsAvailableList.list_dependencies = displayedMods.map(mod => (mod.dependencies || []).join(" "));
modsAvailableList.list = displayedMods.map(mod => mod.i);
modsAvailableList.selected = modsAvailableList.list.indexOf(selectedMod);
}
function clearModList()
{
let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
modsAvailableList.selected = -1;
for (let listIdx of Object.keys(modsAvailableList).filter(key => key.startsWith("list")))
modsAvailableList[listIdx] = [];
}
function selectedModIndex()
{
let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
if (modsAvailableList.selected == -1)
return undefined;
return +modsAvailableList.list[modsAvailableList.selected];
}
function showModDescription()
{
let selected = selectedModIndex();
Engine.GetGUIObjectByName("downloadButton").enabled = selected !== undefined;
Engine.GetGUIObjectByName("modDescription").caption = selected !== undefined ? g_ModsAvailableOnline[selected].summary : "";
}
function cancelModListUpdate()
{
cancelRequest();
if (!g_ModsAvailableOnline.length)
{
closePage();
return;
}
displayMods();
Engine.GetGUIObjectByName('refreshButton').enabled = true;
}
function updateModList()
{
clearModList();
Engine.GetGUIObjectByName("refreshButton").enabled = false;
progressDialog(
translate("Fetching and updating list of available mods."),
translate("Updating"),
false,
translate("Cancel Update"),
cancelModListUpdate);
g_Failure = false;
g_RequestCancelled = false;
Engine.ModIoStartListMods();
}
function downloadMod()
{
let selected = selectedModIndex();
progressDialog(
sprintf(translate("Downloading “%(modname)s”"), {
"modname": g_ModsAvailableOnline[selected].name
}),
translate("Downloading"),
true,
translate("Cancel Download"),
() => { Engine.GetGUIObjectByName("downloadButton").enabled = true; });
Engine.GetGUIObjectByName("downloadButton").enabled = false;
g_Failure = false;
g_RequestCancelled = false;
Engine.ModIoStartDownloadMod(selected);
}
function cancelRequest()
{
g_Failure = false;
g_RequestCancelled = true;
Engine.ModIoCancelRequest();
hideDialog();
}
-function closePage(data)
+function closePage()
{
- Engine.PopGuiPageCB(undefined);
+ Engine.PopGuiPage();
}
function showErrorMessageBox(caption, title, buttonCaptions, buttonActions)
{
if (g_Failure)
return;
messageBox(500, 250, caption, title, buttonCaptions, buttonActions);
g_Failure = true;
}
function progressDialog(dialogCaption, dialogTitle, showProgressBar, buttonCaption, buttonAction)
{
Engine.GetGUIObjectByName("downloadDialog_title").caption = dialogTitle;
let downloadDialog_caption = Engine.GetGUIObjectByName("downloadDialog_caption");
downloadDialog_caption.caption = dialogCaption;
let size = downloadDialog_caption.size;
size.rbottom = showProgressBar ? 40 : 80;
downloadDialog_caption.size = size;
Engine.GetGUIObjectByName("downloadDialog_progress").hidden = !showProgressBar;
Engine.GetGUIObjectByName("downloadDialog_status").hidden = !showProgressBar;
let downloadDialog_button = Engine.GetGUIObjectByName("downloadDialog_button");
downloadDialog_button.caption = buttonCaption;
downloadDialog_button.onPress = () => { cancelRequest(); buttonAction(); };
Engine.GetGUIObjectByName("downloadDialog").hidden = false;
g_RequestStartTime = Date.now();
}
/*
* The "remaining time" and "average speed" texts both naively assume that
* the connection remains relatively stable throughout the download.
*/
function updateProgressBar(progress, totalSize)
{
let progressPercent = Math.ceil(progress * 100);
Engine.GetGUIObjectByName("downloadDialog_progressBar").caption = progressPercent;
let transferredSize = progress * totalSize;
let transferredSizeObj = filesizeToObj(transferredSize);
// Translation: Mod file download indicator. Current size over expected final size, with percentage complete.
Engine.GetGUIObjectByName("downloadDialog_progressText").caption = sprintf(translate("%(current)s / %(total)s (%(percent)s%%)"), {
"current": filesizeToObj(totalSize).unit == transferredSizeObj.unit ? transferredSizeObj.filesize : filesizeToString(transferredSize),
"total": filesizeToString(totalSize),
"percent": progressPercent
});
let elapsedTime = Date.now() - g_RequestStartTime;
let remainingTime = progressPercent ? (100 - progressPercent) * elapsedTime / progressPercent : 0;
let avgSpeed = filesizeToObj(transferredSize / (elapsedTime / 1000));
// Translation: Mod file download status message.
Engine.GetGUIObjectByName("downloadDialog_status").caption = sprintf(translate("Time Elapsed: %(elapsed)s\nEstimated Time Remaining: %(remaining)s\nAverage Speed: %(avgSpeed)s"), {
"elapsed": timeToString(elapsedTime),
"remaining": remainingTime ? timeToString(remainingTime) : translate("∞"),
// Translation: Average download speed, used to give the user a very rough and naive idea of the download time. For example: 123.4 KiB/s
"avgSpeed": sprintf(translate("%(number)s %(unit)s/s"), {
"number": avgSpeed.filesize,
"unit": avgSpeed.unit
})
});
}
function hideDialog()
{
Engine.GetGUIObjectByName("downloadDialog").hidden = true;
}
Index: ps/trunk/binaries/data/mods/mod/gui/modmod/modmodio.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modmod/modmodio.js (revision 22675)
+++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmodio.js (revision 22676)
@@ -1,32 +1,30 @@
function downloadModsButton()
{
initTerms({
"Disclaimer": {
"title": translate("Disclaimer"),
"file": "gui/modio/Disclaimer.txt",
"config": "modio.disclaimer",
"accepted": false,
"callback": openModIo,
"urlButtons": [
{
"caption": translate("mod.io Terms"),
"url": "https://mod.io/terms"
},
{
"caption": translate("mod.io Privacy Policy"),
"url": "https://mod.io/privacy"
}
]
}
});
openTerms("Disclaimer");
}
function openModIo(data)
{
if (data.accepted)
- Engine.PushGuiPage("page_modio.xml", {
- "callback": "initMods"
- });
+ Engine.PushGuiPage("page_modio.xml", {}, initMods);
}
Index: ps/trunk/binaries/data/mods/mod/gui/msgbox/msgbox.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/msgbox/msgbox.js (revision 22675)
+++ ps/trunk/binaries/data/mods/mod/gui/msgbox/msgbox.js (revision 22676)
@@ -1,67 +1,60 @@
/**
* Currently limited to at most 3 buttons per message box.
* The convention is to have "cancel" appear first.
*/
function init(data)
{
// Set title
Engine.GetGUIObjectByName("mbTitleBar").caption = data.title;
// Set subject
let mbTextObj = Engine.GetGUIObjectByName("mbText");
mbTextObj.caption = data.message;
if (data.font)
mbTextObj.font = data.font;
// Default behaviour
let mbCancelHotkey = Engine.GetGUIObjectByName("mbCancelHotkey");
mbCancelHotkey.onPress = Engine.PopGuiPage;
// Calculate size
let mbLRDiff = data.width / 2;
let mbUDDiff = data.height / 2;
Engine.GetGUIObjectByName("mbMain").size = "50%-" + mbLRDiff + " 50%-" + mbUDDiff + " 50%+" + mbLRDiff + " 50%+" + mbUDDiff;
let captions = data.buttonCaptions || [translate("OK")];
// Set button captions and visibility
let mbButton = [];
captions.forEach((caption, i) => {
mbButton[i] = Engine.GetGUIObjectByName("mbButton" + (i + 1));
-
- let action = function()
- {
- if (data.callback)
- Engine.PopGuiPageCB(i);
- else
- Engine.PopGuiPage();
- };
-
mbButton[i].caption = caption;
- mbButton[i].onPress = action;
mbButton[i].hidden = false;
+ mbButton[i].onPress = () => {
+ Engine.PopGuiPage(i);
+ };
// Convention: Cancel is the first button
if (i == 0)
- mbCancelHotkey.onPress = action;
+ mbCancelHotkey.onPress = mbButton[i].onPress;
});
// Distribute buttons horizontally
let y1 = "100%-46";
let y2 = "100%-18";
switch (captions.length)
{
case 1:
mbButton[0].size = "18 " + y1 + " 100%-18 " + y2;
break;
case 2:
mbButton[0].size = "18 " + y1 + " 50%-5 " + y2;
mbButton[1].size = "50%+5 " + y1 + " 100%-18 " + y2;
break;
case 3:
mbButton[0].size = "18 " + y1 + " 33%-5 " + y2;
mbButton[1].size = "33%+5 " + y1 + " 66%-5 " + y2;
mbButton[2].size = "66%+5 " + y1 + " 100%-18 " + y2;
break;
}
}
Index: ps/trunk/binaries/data/mods/mod/gui/termsdialog/termsdialog.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/termsdialog/termsdialog.js (revision 22675)
+++ ps/trunk/binaries/data/mods/mod/gui/termsdialog/termsdialog.js (revision 22676)
@@ -1,89 +1,89 @@
/**
* This implements a basic "Clickwrap agreement", which is an industry standard:
*
* The European Court of Justice decided in the case El Majdoub (case nr C-322/14) that click-wrap agreements are acceptable under certain circumstances
* as proof of the acceptance of terms and conditions (in the meaning of Regulation 44/2001, now replaced by Regulation 1215/2012).
* See https://eur-lex.europa.eu/legal-content/en/TXT/HTML/?uri=uriserv%3AOJ.C_.2015.236.01.0019.01.ENG
* The user should be able to save and print the text of the terms.
*/
var g_TermsPage;
var g_TermsFile;
var g_TermsSprintf;
function init(data)
{
g_TermsPage = data.page;
g_TermsFile = data.file;
g_TermsSprintf = data.sprintf;
Engine.GetGUIObjectByName("title").caption = data.title;
initURLButtons(data.termsURL, data.urlButtons);
initLanguageSelection();
}
function initURLButtons(termsURL, urlButtons)
{
if (termsURL)
urlButtons.unshift({
// Translation: Label of a button that when pressed opens the Terms and Conditions in the default webbrowser.
"caption": translate("View online"),
"url": termsURL
});
urlButtons.forEach((urlButton, i) => {
let button = Engine.GetGUIObjectByName("button[" + i + "]");
button.caption = urlButton.caption;
button.hidden = false;
button.tooltip = sprintf(translate("Open %(url)s in the browser."), {
"url": urlButton.url
});
button.onPress = () => {
openURL(urlButton.url);
};
});
}
function initLanguageSelection()
{
let languageLabel = Engine.GetGUIObjectByName("languageLabel");
let languageLabelWidth = Engine.GetTextWidth(languageLabel.font, languageLabel.caption);
languageLabel.size = "0 0 " + languageLabelWidth + " 100%";
let languageDropdown = Engine.GetGUIObjectByName("languageDropdown");
languageDropdown.size = (languageLabelWidth + 10) + " 4 100% 100%";
languageDropdown.list = (() => {
let displayNames = Engine.GetSupportedLocaleDisplayNames();
let baseNames = Engine.GetSupportedLocaleBaseNames();
// en-US
let list = [displayNames[0]];
// current locale
let currentLocaleDict = Engine.GetFallbackToAvailableDictLocale(Engine.GetCurrentLocale());
if (currentLocaleDict != baseNames[0])
list.push(displayNames[baseNames.indexOf(currentLocaleDict)]);
return list;
})();
languageDropdown.onSelectionChange = () => {
Engine.GetGUIObjectByName("mainText").caption =
sprintf(
languageDropdown.selected == 1 ?
Engine.TranslateLines(Engine.ReadFile(g_TermsFile)) :
Engine.ReadFile(g_TermsFile),
g_TermsSprintf);
};
languageDropdown.selected = languageDropdown.list.length - 1;
}
function closeTerms(accepted)
{
- Engine.PopGuiPageCB({
+ Engine.PopGuiPage({
"page": g_TermsPage,
"accepted": accepted
});
}
Index: ps/trunk/binaries/data/mods/public/gui/aiconfig/aiconfig.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/aiconfig/aiconfig.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/aiconfig/aiconfig.js (revision 22676)
@@ -1,83 +1,80 @@
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
*/
const g_IsController = !Engine.HasNetClient() || Engine.HasNetServer();
var g_PlayerSlot;
var g_AIDescriptions = [{
"id": "",
"data": {
"name": translateWithContext("ai", "None"),
"description": translate("AI will be disabled for this player.")
}
}].concat(g_Settings.AIDescriptions);
var g_AIControls = {
"aiSelection": {
"labels": g_AIDescriptions.map(ai => ai.data.name),
"selected": settings => g_AIDescriptions.findIndex(ai => ai.id == settings.id)
},
"aiDifficulty": {
"labels": prepareForDropdown(g_Settings.AIDifficulties).Title,
"selected": settings => settings.difficulty
},
"aiBehavior": {
"labels": prepareForDropdown(g_Settings.AIBehaviors).Title,
"selected": settings => g_Settings.AIBehaviors.findIndex(b => b.Name == settings.behavior)
}
};
function init(settings)
{
// Remember the player ID that we change the AI settings for
g_PlayerSlot = settings.playerSlot;
for (let name in g_AIControls)
{
let control = Engine.GetGUIObjectByName(name);
control.list = g_AIControls[name].labels;
control.selected = g_AIControls[name].selected(settings);
control.hidden = !g_IsController;
let label = Engine.GetGUIObjectByName(name + "Text");
label.caption = control.list[control.selected];
label.hidden = g_IsController;
}
checkBehavior();
}
function selectAI(idx)
{
Engine.GetGUIObjectByName("aiDescription").caption = g_AIDescriptions[idx].data.description;
}
/** Behavior choice does not apply for Sandbox level */
function checkBehavior()
{
if (g_Settings.AIDifficulties[Engine.GetGUIObjectByName("aiDifficulty").selected].Name != "sandbox")
{
Engine.GetGUIObjectByName("aiBehavior").enabled = true;
return;
}
let aiBehavior = Engine.GetGUIObjectByName("aiBehavior");
aiBehavior.enabled = false;
aiBehavior.selected = g_Settings.AIBehaviors.findIndex(b => b.Name == "balanced");
}
function returnAI(save = true)
{
let idx = Engine.GetGUIObjectByName("aiSelection").selected;
-
- // Pop the page before calling the callback, so the callback runs
- // in the parent GUI page's context
- Engine.PopGuiPageCB({
+ Engine.PopGuiPage({
"save": save,
"id": g_AIDescriptions[idx].id,
"name": g_AIDescriptions[idx].data.name,
"difficulty": Engine.GetGUIObjectByName("aiDifficulty").selected,
"behavior": g_Settings.AIBehaviors[Engine.GetGUIObjectByName("aiBehavior").selected].Name,
"playerSlot": g_PlayerSlot
});
}
Index: ps/trunk/binaries/data/mods/public/gui/civinfo/civinfo.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/civinfo/civinfo.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/civinfo/civinfo.js (revision 22676)
@@ -1,164 +1,152 @@
/**
* Display selectable civs only.
*/
const g_CivData = loadCivData(true, false);
-/**
- * Callback function name on closing gui via Engine.PopGuiPage().
- */
-var g_Callback = "";
-
var g_SelectedCiv = "";
/**
* Initialize the dropdown containing all the available civs.
*/
function init(data = {})
{
- if (data.callback)
- g_Callback = data.callback;
-
var civList = Object.keys(g_CivData).map(civ => ({ "name": g_CivData[civ].Name, "code": civ })).sort(sortNameIgnoreCase);
var civSelection = Engine.GetGUIObjectByName("civSelection");
civSelection.list = civList.map(civ => civ.name);
civSelection.list_data = civList.map(civ => civ.code);
civSelection.selected = data.civ ? civSelection.list_data.indexOf(data.civ) : 0;
Engine.GetGUIObjectByName("structreeButton").tooltip = colorizeHotkey(translate("%(hotkey)s: Switch to Structure Tree."), "structree");
Engine.GetGUIObjectByName("close").tooltip = colorizeHotkey(translate("%(hotkey)s: Close History."), "cancel");
}
/**
* Give the first character a larger font.
*/
function bigFirstLetter(str, size)
{
return '[font="sans-bold-' + (size + 6) + '"]' + str[0] + '[/font]' + '[font="sans-bold-' + size + '"]' + str.substring(1) + '[/font]';
}
/**
* Set heading font - bold and mixed caps
*
* @param string {string}
* @param size {number} - Font size
* @returns {string}
*/
function heading(string, size)
{
var textArray = string.split(" ");
for (let i in textArray)
{
var word = textArray[i];
var wordCaps = word.toUpperCase();
// Check if word is capitalized, if so assume it needs a big first letter
// Check if toLowerCase changes the character to avoid false positives from special signs
if (word.length && word[0].toLowerCase() != word[0])
textArray[i] = bigFirstLetter(wordCaps, size);
else
textArray[i] = '[font="sans-bold-' + size + '"]' + wordCaps + '[/font]'; // TODO: Would not be necessary if we could do nested tags
}
return textArray.join(" ");
}
/**
* Prepends a backslash to all quotation marks.
* @param str {string}
* @returns {string}
*/
function escapeQuotation(str)
{
return str.replace(/"/g, "\\\"");
}
/**
* Returns a styled concatenation of Name, History and Description of the given object.
*
* @param obj {Object}
* @returns {string}
*/
function subHeading(obj)
{
if (!obj.Name)
return "";
let string = '[font="sans-bold-14"]' + obj.Name + '[/font] ';
if (obj.History)
string += '[icon="iconInfo" tooltip="' + escapeQuotation(obj.History) + '" tooltip_style="civInfoTooltip"]';
if (obj.Description)
string += '\n ' + obj.Description;
return coloredText(string + "\n", "white");
}
function switchToStrucTreePage()
{
- Engine.PopGuiPage();
- Engine.PushGuiPage("page_structree.xml", { "civ": g_SelectedCiv, "callback": g_Callback });
+ Engine.PopGuiPage({ "civ": g_SelectedCiv, "nextPage": "page_structree.xml" });
}
function closePage()
{
- if (g_Callback)
- Engine.PopGuiPageCB({ "civ": g_SelectedCiv, "page": "page_civinfo.xml" });
- else
- Engine.PopGuiPage();
+ Engine.PopGuiPage({ "civ": g_SelectedCiv, "page": "page_civinfo.xml" });
}
/**
* Updates the GUI after the user selected a civ from dropdown.
*
* @param code {string}
*/
function selectCiv(code)
{
var civInfo = g_CivData[code];
g_SelectedCiv = code;
if(!civInfo)
error(sprintf("Error loading civ data for \"%(code)s\"", { "code": code }));
// Update civ gameplay display
Engine.GetGUIObjectByName("civGameplayHeading").caption = heading(sprintf(translate("%(civilization)s Gameplay"), { "civilization": civInfo.Name }), 16);
// Bonuses
var bonusCaption = heading(translatePlural("Civilization Bonus", "Civilization Bonuses", civInfo.CivBonuses.length), 12) + '\n';
for (let bonus of civInfo.CivBonuses)
bonusCaption += subHeading(bonus);
// Team Bonuses
bonusCaption += heading(translatePlural("Team Bonus", "Team Bonuses", civInfo.TeamBonuses.length), 12) + '\n';
for (let bonus of civInfo.TeamBonuses)
bonusCaption += subHeading(bonus);
Engine.GetGUIObjectByName("civBonuses").caption = bonusCaption;
// Special techs
var techCaption = heading(translate("Special Technologies"), 12) + '\n';
for (let faction of civInfo.Factions)
for (let technology of faction.Technologies)
techCaption += subHeading(technology);
// Special buildings
techCaption += heading(translatePlural("Special Building", "Special Buildings", civInfo.Structures.length), 12) + '\n';
for (let structure of civInfo.Structures)
techCaption += subHeading(structure);
Engine.GetGUIObjectByName("civTechs").caption = techCaption;
// Heroes
var heroCaption = heading(translate("Heroes"), 12) + '\n';
for (let faction of civInfo.Factions)
{
for (let hero of faction.Heroes)
heroCaption += subHeading(hero);
heroCaption += '\n';
}
Engine.GetGUIObjectByName("civHeroes").caption = heroCaption;
// Update civ history display
Engine.GetGUIObjectByName("civHistoryHeading").caption = heading(sprintf(translate("History of the %(civilization)s"), { "civilization": civInfo.Name }), 16);
Engine.GetGUIObjectByName("civHistoryText").caption = civInfo.History;
}
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 22676)
@@ -1,2780 +1,2781 @@
const g_MatchSettings_SP = "config/matchsettings.json";
const g_MatchSettings_MP = "config/matchsettings.mp.json";
const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_TriggerDifficulties = prepareForDropdown(g_Settings && g_Settings.TriggerDifficulties);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
var g_GameSpeeds = getGameSpeedChoices(false);
/**
* Offer users to select playable civs only.
* Load unselectable civs as they could appear in scenario maps.
*/
const g_CivData = loadCivData(false, false);
/**
* Store civilization code and page (structree or history) opened in civilization info.
*/
var g_CivInfo = {
- "code": "",
+ "civ": "",
"page": "page_civinfo.xml"
};
/**
* Highlight the "random" dropdownlist item.
*/
var g_ColorRandom = "orange";
/**
* Color for regular dropdownlist items.
*/
var g_ColorRegular = "white";
/**
* Color for "Unassigned"-placeholder item in the dropdownlist.
*/
var g_PlayerAssignmentColors = {
"player": g_ColorRegular,
"observer": "170 170 250",
"unassigned": "140 140 140",
"AI": "70 150 70"
};
/**
* Used for highlighting the sender of chat messages.
*/
var g_SenderFont = "sans-bold-13";
/**
* This yields [1, 2, ..., MaxPlayers].
*/
var g_NumPlayersList = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1);
/**
* Used for generating the botnames.
*/
var g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
var g_PlayerTeamList = prepareForDropdown([{
"label": translateWithContext("team", "None"),
"id": -1
}].concat(
Array(g_MaxTeams).fill(0).map((v, i) => ({
"label": i + 1,
"id": i
}))
)
);
/**
* Number of relics: [1, ..., NumCivs]
*/
var g_RelicCountList = Object.keys(g_CivData).map((civ, i) => i + 1);
var g_PlayerCivList = g_CivData && prepareForDropdown([{
"name": translateWithContext("civilization", "Random"),
"tooltip": translate("Picks one civilization at random when the game starts."),
"color": g_ColorRandom,
"code": "random"
}].concat(
Object.keys(g_CivData).filter(
civ => g_CivData[civ].SelectableInGameSetup
).map(civ => ({
"name": g_CivData[civ].Name,
"tooltip": g_CivData[civ].History,
"color": g_ColorRegular,
"code": civ
})).sort(sortNameIgnoreCase)
)
);
/**
* All selectable playercolors except gaia.
*/
var g_PlayerColorPickerList = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
/**
* Directory containing all maps of the given type.
*/
var g_MapPath = {
"random": "maps/random/",
"scenario": "maps/scenarios/",
"skirmish": "maps/skirmishes/"
};
/**
* Containing the colors to highlight the ready status of players,
* the chat ready messages and
* the tooltips and captions for the ready button
*/
var g_ReadyData = [
{
"color": g_ColorRegular,
"chat": translate("* %(username)s is not ready."),
"caption": translate("I'm ready"),
"tooltip": translate("State that you are ready to play.")
},
{
"color": "green",
"chat": translate("* %(username)s is ready!"),
"caption": translate("Stay ready"),
"tooltip": translate("Stay ready even when the game settings change.")
},
{
"color": "150 150 250",
"chat": "",
"caption": translate("I'm not ready!"),
"tooltip": translate("State that you are not ready to play.")
}
];
/**
* Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
*/
var g_NetMessageTypes = {
"netstatus": msg => handleNetStatusMessage(msg),
"netwarn": msg => addNetworkWarning(msg),
"gamesetup": msg => handleGamesetupMessage(msg),
"players": msg => handlePlayerAssignmentMessage(msg),
"ready": msg => handleReadyMessage(msg),
"start": msg => handleGamestartMessage(msg),
"kicked": msg => addChatMessage({
"type": msg.banned ? "banned" : "kicked",
"username": msg.username
}),
"chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text }),
};
var g_FormatChatMessage = {
"system": (msg, user) => systemMessage(msg.text),
"settings": (msg, user) => systemMessage(translate('Game settings have been changed')),
"connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })),
"disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })),
"kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })),
"banned": (msg, user) => systemMessage(sprintf(translate("%(username)s has been banned"), { "username": user })),
"chat": (msg, user) => sprintf(translate("%(username)s %(message)s"), {
"username": senderFont(sprintf(translate("<%(username)s>"), { "username": user })),
"message": escapeText(msg.text || "")
}),
"ready": (msg, user) => sprintf(g_ReadyData[msg.status].chat, { "username": user }),
"clientlist": (msg, user) => getUsernameList(),
};
var g_MapFilters = [
{
"id": "default",
"name": translateWithContext("map filter", "Default"),
"tooltip": translateWithContext("map filter", "All maps except naval and demo maps."),
"filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1),
"Default": true
},
{
"id": "naval",
"name": translate("Naval Maps"),
"tooltip": translateWithContext("map filter", "Maps where ships are needed to reach the enemy."),
"filter": mapKeywords => mapKeywords.indexOf("naval") != -1
},
{
"id": "demo",
"name": translate("Demo Maps"),
"tooltip": translateWithContext("map filter", "These maps are not playable but for demonstration purposes only."),
"filter": mapKeywords => mapKeywords.indexOf("demo") != -1
},
{
"id": "new",
"name": translate("New Maps"),
"tooltip": translateWithContext("map filter", "Maps that are brand new in this release of the game."),
"filter": mapKeywords => mapKeywords.indexOf("new") != -1
},
{
"id": "trigger",
"name": translate("Trigger Maps"),
"tooltip": translateWithContext("map filter", "Maps that come with scripted events and potentially spawn enemy units."),
"filter": mapKeywords => mapKeywords.indexOf("trigger") != -1
},
{
"id": "all",
"name": translate("All Maps"),
"tooltip": translateWithContext("map filter", "Every map of the chosen maptype."),
"filter": mapKeywords => true
},
];
/**
* This contains only filters that have at least one map matching them.
*/
var g_MapFilterList;
/**
* Array of biome identifiers supported by the currently selected map.
*/
var g_BiomeList;
/**
* Array of trigger difficulties identifiers supported by the currently selected map.
*/
var g_TriggerDifficultyList;
/**
* Whether this is a single- or multiplayer match.
*/
const g_IsNetworked = Engine.HasNetClient();
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
*/
const g_IsController = !g_IsNetworked || Engine.HasNetServer();
/**
* Whether this is a tutorial.
*/
var g_IsTutorial;
/**
* To report the game to the lobby bot.
*/
var g_ServerName;
var g_ServerPort;
/**
* IP address and port of the STUN endpoint.
*/
var g_StunEndpoint;
/**
* States whether the GUI is currently updated in response to network messages instead of user input
* and therefore shouldn't send further messages to the network.
*/
var g_IsInGuiUpdate = false;
/**
* Whether the current player is ready to start the game.
* 0 - not ready
* 1 - ready
* 2 - stay ready
*/
var g_IsReady = 0;
/**
* Ignore duplicate ready commands on init.
*/
var g_ReadyInit = true;
/**
* If noone has changed the ready status, we have no need to spam the settings changed message.
*
* <=0 - Suppressed settings message
* 1 - Will show settings message
* 2 - Host's initial ready, suppressed settings message
*/
var g_ReadyChanged = 2;
/**
* Used to prevent calling resetReadyData when starting a game or doubleclicking on the "Start Game" button.
*/
var g_GameStarted = false;
/**
* Selectable options (player, AI, unassigned) in the player assignment dropdowns and
* their colorized, textual representation.
*/
var g_PlayerAssignmentList = {};
/**
* Remembers which clients are assigned to which player slots and whether they are ready.
* The keys are guids or "local" in Singleplayer.
*/
var g_PlayerAssignments = {};
var g_DefaultPlayerData = [];
var g_GameAttributes = { "settings": {} };
/**
* List of translated words that can be used to autocomplete titles of settings
* and their values (for example playernames).
*/
var g_Autocomplete = [];
/**
* Array of strings formatted as displayed, including playername.
*/
var g_ChatMessages = [];
/**
* Minimum amount of pixels required for the chat panel to be visible.
*/
var g_MinChatWidth = 96;
/**
* Horizontal space between chat window and settings.
*/
var g_ChatSettingsMargin = 10;
/**
* Filename and translated title of all maps, given the currently selected
* maptype and filter. Sorted by title, shown in the dropdown.
*/
var g_MapSelectionList = [];
/**
* Cache containing the mapsettings. Just-in-time loading.
*/
var g_MapData = {};
/**
* Wait one tick before initializing the GUI objects and
* don't process netmessages prior to that.
*/
var g_LoadingState = 0;
/**
* Send the current gamesettings to the lobby bot if the settings didn't change for this number of seconds.
*/
var g_GameStanzaTimeout = 2;
/**
* Index of the GUI timer.
*/
var g_GameStanzaTimer;
/**
* Only send a lobby update if something actually changed.
*/
var g_LastGameStanza;
/**
* Remembers if the current player viewed the AI settings of some playerslot.
*/
var g_LastViewedAIPlayer = -1;
/**
* Total number of units that the engine can run with smoothly.
* It means a 4v4 with 150 population can still run nicely, but more than that might "lag".
*/
var g_PopulationCapacityRecommendation = 1200;
/**
* Horizontal space between tab buttons and lobby button.
*/
var g_LobbyButtonSpacing = 8;
/**
* Vertical size of a tab button.
*/
var g_TabButtonHeight = 30;
/**
* Vertical space between two tab buttons.
*/
var g_TabButtonDist = 4;
/**
* Vertical size of a setting object.
*/
var g_SettingHeight = 32;
/**
* Vertical space between two setting objects.
*/
var g_SettingDist = 2;
/**
* Maximum width of a column in the settings panel.
*/
var g_MaxColumnWidth = 470;
/**
* Pixels per millisecond the settings panel slides when opening/closing.
*/
var g_SlideSpeed = 1.2;
/**
* Store last tick time.
*/
var g_LastTickTime = Date.now();
/**
* Order in which the GUI elements will be shown.
* All valid settings are required to appear here.
*/
var g_SettingsTabsGUI = [
{
"label": translateWithContext("Match settings tab name", "Map"),
"settings": [
"mapType",
"mapFilter",
"mapSelection",
"mapSize",
"biome",
"triggerDifficulty",
"nomad",
"disableTreasures",
"exploreMap",
"revealMap"
]
},
{
"label": translateWithContext("Match settings tab name", "Player"),
"settings": [
"numPlayers",
"populationCap",
"startingResources",
"disableSpies",
"enableCheats"
]
},
{
"label": translateWithContext("Match settings tab name", "Game Type"),
"settings": [
...g_VictoryConditions.map(victoryCondition => victoryCondition.Name),
"relicCount",
"relicDuration",
"wonderDuration",
"regicideGarrison",
"gameSpeed",
"ceasefire",
"lockTeams",
"lastManStanding",
"enableRating"
]
}
];
/**
* Contains the logic of all multiple-choice gamesettings.
*
* Logic
* ids - Array of identifier strings that indicate the selected value.
* default - Returns the index of the default value (not the value itself).
* defined - Whether a value for the setting is actually specified.
* get - The identifier of the currently selected value.
* select - Saves and processes the value of the selected index of the ids array.
*
* GUI
* title - The caption shown in the label.
* tooltip - A description shown when hovering the dropdown or a specific item.
* labels - Array of translated strings selectable for this dropdown.
* colors - Optional array of colors to tint the according dropdown items with.
* hidden - If hidden, both the label and dropdown won't be visible. (default: false)
* enabled - Only the label will be shown if the setting is disabled. (default: true)
* autocomplete - Marks whether to autocomplete translated values of the string. (default: undefined)
* If not undefined, must be a number that denotes the priority (higher numbers come first).
* If undefined, still autocompletes the translated title of the setting.
* initOrder - Settings with lower values will be initialized first.
*/
var g_Dropdowns = {
"mapType": {
"title": () => translate("Map Type"),
"tooltip": (hoverIdx) => g_MapTypes.Description[hoverIdx] || translate("Select a map type."),
"labels": () => g_MapTypes.Title,
"ids": () => g_MapTypes.Name,
"default": () => g_MapTypes.Default,
"defined": () => g_GameAttributes.mapType !== undefined,
"get": () => g_GameAttributes.mapType,
"select": (itemIdx) => {
g_MapData = {};
g_GameAttributes.mapType = g_MapTypes.Name[itemIdx];
g_GameAttributes.mapPath = g_MapPath[g_GameAttributes.mapType];
delete g_GameAttributes.map;
if (g_GameAttributes.mapType != "scenario")
g_GameAttributes.settings = {
"PlayerData": clone(g_DefaultPlayerData.slice(0, 4))
};
reloadMapFilterList();
},
"autocomplete": 0,
"initOrder": 1
},
"mapFilter": {
"title": () => translate("Map Filter"),
"tooltip": (hoverIdx) => g_MapFilterList.tooltip[hoverIdx] || translate("Select a map filter."),
"labels": () => g_MapFilterList.name,
"ids": () => g_MapFilterList.id,
"default": () => g_MapFilterList.Default,
"defined": () => g_MapFilterList.id.indexOf(g_GameAttributes.mapFilter || "") != -1,
"get": () => g_GameAttributes.mapFilter,
"select": (itemIdx) => {
g_GameAttributes.mapFilter = g_MapFilterList.id[itemIdx];
delete g_GameAttributes.map;
reloadMapList();
},
"autocomplete": 0,
"initOrder": 2
},
"mapSelection": {
"title": () => translate("Select Map"),
"tooltip": (hoverIdx) => g_MapSelectionList.description[hoverIdx] || translate("Select a map to play on."),
"labels": () => g_MapSelectionList.name,
"colors": () => g_MapSelectionList.color,
"ids": () => g_MapSelectionList.file,
"default": () => 0,
"defined": () => g_GameAttributes.map !== undefined,
"get": () => g_GameAttributes.map,
"select": (itemIdx) => {
selectMap(g_MapSelectionList.file[itemIdx]);
},
"autocomplete": 0,
"initOrder": 3
},
"mapSize": {
"title": () => translate("Map Size"),
"tooltip": (hoverIdx) => g_MapSizes.Tooltip[hoverIdx] || translate("Select map size. (Larger sizes may reduce performance.)"),
"labels": () => g_MapSizes.Name,
"ids": () => g_MapSizes.Tiles,
"default": () => g_MapSizes.Default,
"defined": () => g_GameAttributes.settings.Size !== undefined,
"get": () => g_GameAttributes.settings.Size,
"select": (itemIdx) => {
g_GameAttributes.settings.Size = g_MapSizes.Tiles[itemIdx];
},
"hidden": () => g_GameAttributes.mapType != "random",
"autocomplete": 0,
"initOrder": 1000
},
"biome": {
"title": () => translate("Biome"),
"tooltip": (hoverIdx) => g_BiomeList && g_BiomeList.Description && g_BiomeList.Description[hoverIdx] || translate("Select the flora and fauna."),
"labels": () => g_BiomeList ? g_BiomeList.Title : [],
"colors": (itemIdx) => g_BiomeList ? g_BiomeList.Color : [],
"ids": () => g_BiomeList ? g_BiomeList.Id : [],
"default": () => 0,
"defined": () => g_GameAttributes.settings.Biome !== undefined,
"get": () => g_GameAttributes.settings.Biome,
"select": (itemIdx) => {
g_GameAttributes.settings.Biome = g_BiomeList && g_BiomeList.Id[itemIdx];
},
"hidden": () => !g_BiomeList,
"autocomplete": 0,
"initOrder": 1000
},
"numPlayers": {
"title": () => translate("Number of Players"),
"tooltip": (hoverIdx) => translate("Select number of players."),
"labels": () => g_NumPlayersList,
"ids": () => g_NumPlayersList,
"default": () => g_MaxPlayers - 1,
"defined": () => g_GameAttributes.settings.PlayerData !== undefined,
"get": () => g_GameAttributes.settings.PlayerData.length,
"enabled": () => g_GameAttributes.mapType == "random",
"select": (itemIdx) => {
let num = itemIdx + 1;
let pData = g_GameAttributes.settings.PlayerData;
g_GameAttributes.settings.PlayerData =
num > pData.length ?
pData.concat(clone(g_DefaultPlayerData.slice(pData.length, num))) :
pData.slice(0, num);
unassignInvalidPlayers(num);
sanitizePlayerData(g_GameAttributes.settings.PlayerData);
},
"initOrder": 1000
},
"populationCap": {
"title": () => translate("Population Cap"),
"tooltip": (hoverIdx) => {
let popCap = g_PopulationCapacities.Population[hoverIdx];
let players = g_GameAttributes.settings.PlayerData.length;
if (hoverIdx == -1 || popCap * players <= g_PopulationCapacityRecommendation)
return translate("Select population limit.");
return coloredText(
sprintf(translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population."), {
"players": players,
"popCap": popCap
}),
"orange");
},
"labels": () => g_PopulationCapacities.Title,
"ids": () => g_PopulationCapacities.Population,
"default": () => g_PopulationCapacities.Default,
"defined": () => g_GameAttributes.settings.PopulationCap !== undefined,
"get": () => g_GameAttributes.settings.PopulationCap,
"select": (itemIdx) => {
g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[itemIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"startingResources": {
"title": () => translate("Starting Resources"),
"tooltip": (hoverIdx) => {
return hoverIdx >= 0 ?
sprintf(translate("Initial amount of each resource: %(resources)s."), {
"resources": g_StartingResources.Resources[hoverIdx]
}) :
translate("Select the game's starting resources.");
},
"labels": () => g_StartingResources.Title,
"ids": () => g_StartingResources.Resources,
"default": () => g_StartingResources.Default,
"defined": () => g_GameAttributes.settings.StartingResources !== undefined,
"get": () => g_GameAttributes.settings.StartingResources,
"select": (itemIdx) => {
g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[itemIdx];
},
"hidden": () => g_GameAttributes.mapType == "scenario",
"autocomplete": 0,
"initOrder": 1000
},
"ceasefire": {
"title": () => translate("Ceasefire"),
"tooltip": (hoverIdx) => translate("Set time where no attacks are possible."),
"labels": () => g_Ceasefire.Title,
"ids": () => g_Ceasefire.Duration,
"default": () => g_Ceasefire.Default,
"defined": () => g_GameAttributes.settings.Ceasefire !== undefined,
"get": () => g_GameAttributes.settings.Ceasefire,
"select": (itemIdx) => {
g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[itemIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"relicCount": {
"title": () => translate("Relic Count"),
"tooltip": (hoverIdx) => translate("Total number of relics spawned on the map. Relic victory is most realistic with only one or two relics. With greater numbers, the relics are important to capture to receive aura bonuses."),
"labels": () => g_RelicCountList,
"ids": () => g_RelicCountList,
"default": () => g_RelicCountList.indexOf(2),
"defined": () => g_GameAttributes.settings.RelicCount !== undefined,
"get": () => g_GameAttributes.settings.RelicCount,
"select": (itemIdx) => {
g_GameAttributes.settings.RelicCount = g_RelicCountList[itemIdx];
},
"hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1,
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"relicDuration": {
"title": () => translate("Relic Duration"),
"tooltip": (hoverIdx) => translate("Minutes until the player has achieved Relic Victory."),
"labels": () => g_VictoryDurations.Title,
"ids": () => g_VictoryDurations.Duration,
"default": () => g_VictoryDurations.Default,
"defined": () => g_GameAttributes.settings.RelicDuration !== undefined,
"get": () => g_GameAttributes.settings.RelicDuration,
"select": (itemIdx) => {
g_GameAttributes.settings.RelicDuration = g_VictoryDurations.Duration[itemIdx];
},
"hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1,
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"wonderDuration": {
"title": () => translate("Wonder Duration"),
"tooltip": (hoverIdx) => translate("Minutes until the player has achieved Wonder Victory."),
"labels": () => g_VictoryDurations.Title,
"ids": () => g_VictoryDurations.Duration,
"default": () => g_VictoryDurations.Default,
"defined": () => g_GameAttributes.settings.WonderDuration !== undefined,
"get": () => g_GameAttributes.settings.WonderDuration,
"select": (itemIdx) => {
g_GameAttributes.settings.WonderDuration = g_VictoryDurations.Duration[itemIdx];
},
"hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("wonder") == -1,
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"gameSpeed": {
"title": () => translate("Game Speed"),
"tooltip": (hoverIdx) => translate("Select game speed."),
"labels": () => g_GameSpeeds.Title,
"ids": () => g_GameSpeeds.Speed,
"default": () => g_GameSpeeds.Default,
"defined": () =>
g_GameAttributes.gameSpeed !== undefined &&
g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) != -1,
"get": () => g_GameAttributes.gameSpeed,
"select": (itemIdx) => {
g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[itemIdx];
},
"initOrder": 1000
},
"triggerDifficulty": {
"title": () => translate("Difficulty"),
"tooltip": (hoverIdx) => g_TriggerDifficultyList && g_TriggerDifficultyList.Description[hoverIdx] ||
translate("Select the difficulty of this scenario."),
"labels": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Title : [],
"ids": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Id : [],
"default": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Default : 0,
"defined": () => g_GameAttributes.settings.TriggerDifficulty !== undefined,
"get": () => g_GameAttributes.settings.TriggerDifficulty,
"select": (itemIdx) => {
g_GameAttributes.settings.TriggerDifficulty = g_TriggerDifficultyList && g_TriggerDifficultyList.Id[itemIdx];
},
"hidden": () => !g_TriggerDifficultyList,
"initOrder": 1000
},
};
/**
* These dropdowns provide a setting that is repeated once for each player
* (where playerIdx is starting from 0 for player 1).
*/
var g_PlayerDropdowns = {
"playerAssignment": {
"tooltip": (playerIdx) => translate("Select player."),
"labels": (playerIdx) => g_PlayerAssignmentList.Name || [],
"colors": (playerIdx) => g_PlayerAssignmentList.Color || [],
"ids": (playerIdx) => g_PlayerAssignmentList.Choice || [],
"default": (playerIdx) => "ai:petra",
"defined": (playerIdx) => playerIdx < g_GameAttributes.settings.PlayerData.length,
"get": (playerIdx) => {
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player == playerIdx + 1)
return "guid:" + guid;
for (let ai of g_Settings.AIDescriptions)
if (g_GameAttributes.settings.PlayerData[playerIdx].AI == ai.id)
return "ai:" + ai.id;
return "unassigned";
},
"select": (selectedIdx, playerIdx) => {
let choice = g_PlayerAssignmentList.Choice[selectedIdx];
if (choice == "unassigned" || choice.startsWith("ai:"))
{
if (g_IsNetworked)
Engine.AssignNetworkPlayer(playerIdx+1, "");
else if (g_PlayerAssignments.local.player == playerIdx+1)
g_PlayerAssignments.local.player = -1;
g_GameAttributes.settings.PlayerData[playerIdx].AI = choice.startsWith("ai:") ? choice.substr(3) : "";
}
else
swapPlayers(choice.substr("guid:".length), playerIdx);
},
"autocomplete": 100,
},
"playerTeam": {
"tooltip": (playerIdx) => translate("Select player's team."),
"labels": (playerIdx) => g_PlayerTeamList.label,
"ids": (playerIdx) => g_PlayerTeamList.id,
"default": (playerIdx) => 0,
"defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team !== undefined,
"get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team,
"select": (selectedIdx, playerIdx) => {
g_GameAttributes.settings.PlayerData[playerIdx].Team = selectedIdx - 1;
},
"enabled": () => g_GameAttributes.mapType != "scenario",
},
"playerCiv": {
"tooltip": (hoverIdx, playerIdx) => g_PlayerCivList.tooltip[hoverIdx] || translate("Choose the civilization for this player."),
"labels": (playerIdx) => g_PlayerCivList.name,
"colors": (playerIdx) => g_PlayerCivList.color,
"ids": (playerIdx) => g_PlayerCivList.code,
"default": (playerIdx) => 0,
"defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ !== undefined,
"get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ,
"select": (selectedIdx, playerIdx) => {
g_GameAttributes.settings.PlayerData[playerIdx].Civ = g_PlayerCivList.code[selectedIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"autocomplete": 90,
},
"playerColorPicker": {
"tooltip": (playerIdx) => translate("Pick a color."),
"labels": (playerIdx) => g_PlayerColorPickerList.map(color => "■"),
"colors": (playerIdx) => g_PlayerColorPickerList.map(color => rgbToGuiColor(color)),
"ids": (playerIdx) => g_PlayerColorPickerList.map((color, index) => index),
"default": (playerIdx) => playerIdx,
"defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Color !== undefined,
"get": (playerIdx) => g_PlayerColorPickerList.indexOf(g_GameAttributes.settings.PlayerData[playerIdx].Color),
"select": (selectedIdx, playerIdx) => {
let playerData = g_GameAttributes.settings.PlayerData;
// If someone else has that color, give that player the old color
let sameColorPData = playerData.find(pData => sameColor(g_PlayerColorPickerList[selectedIdx], pData.Color));
if (sameColorPData)
sameColorPData.Color = playerData[playerIdx].Color;
playerData[playerIdx].Color = g_PlayerColorPickerList[selectedIdx];
ensureUniquePlayerColors(playerData);
},
"enabled": () => g_GameAttributes.mapType != "scenario",
},
};
/**
* Contains the logic of all boolean gamesettings.
*/
var g_Checkboxes = Object.assign(
{},
g_VictoryConditions.reduce((obj, victoryCondition) => {
obj[victoryCondition.Name] = {
"title": () => victoryCondition.Title,
"tooltip": () => victoryCondition.Description,
// Defaults are set in supplementDefault directly from g_VictoryConditions since we use an array
"defined": () => true,
"get": () => g_GameAttributes.settings.VictoryConditions.indexOf(victoryCondition.Name) != -1,
"set": checked => {
if (checked)
{
g_GameAttributes.settings.VictoryConditions.push(victoryCondition.Name);
if (victoryCondition.ChangeOnChecked)
for (let setting in victoryCondition.ChangeOnChecked)
g_Checkboxes[setting].set(victoryCondition.ChangeOnChecked[setting]);
}
else
g_GameAttributes.settings.VictoryConditions = g_GameAttributes.settings.VictoryConditions.filter(victoryConditionName => victoryConditionName != victoryCondition.Name);
},
"enabled": () =>
g_GameAttributes.mapType != "scenario" &&
(!victoryCondition.DisabledWhenChecked ||
victoryCondition.DisabledWhenChecked.every(victoryConditionName => g_GameAttributes.settings.VictoryConditions.indexOf(victoryConditionName) == -1))
};
return obj;
}, {}),
{
"regicideGarrison": {
"title": () => translate("Hero Garrison"),
"tooltip": () => translate("Toggle whether heroes can be garrisoned."),
"default": () => false,
"defined": () => g_GameAttributes.settings.RegicideGarrison !== undefined,
"get": () => g_GameAttributes.settings.RegicideGarrison,
"set": checked => {
g_GameAttributes.settings.RegicideGarrison = checked;
},
"hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("regicide") == -1,
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"nomad": {
"title": () => translate("Nomad"),
"tooltip": () => translate("In Nomad mode, players start with only few units and have to find a suitable place to build their city. Ceasefire is recommended."),
"default": () => false,
"defined": () => g_GameAttributes.settings.Nomad !== undefined,
"get": () => g_GameAttributes.settings.Nomad,
"set": checked => {
g_GameAttributes.settings.Nomad = checked;
},
"hidden": () => g_GameAttributes.mapType != "random",
"initOrder": 1000
},
"revealMap": {
"title": () =>
// Translation: Make sure to differentiate between the revealed map and explored map settings!
translate("Revealed Map"),
"tooltip":
// Translation: Make sure to differentiate between the revealed map and explored map settings!
() => translate("Toggle revealed map (see everything)."),
"default": () => false,
"defined": () => g_GameAttributes.settings.RevealMap !== undefined,
"get": () => g_GameAttributes.settings.RevealMap,
"set": checked => {
g_GameAttributes.settings.RevealMap = checked;
if (checked)
g_Checkboxes.exploreMap.set(true);
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"exploreMap": {
"title":
// Translation: Make sure to differentiate between the revealed map and explored map settings!
() => translate("Explored Map"),
"tooltip":
// Translation: Make sure to differentiate between the revealed map and explored map settings!
() => translate("Toggle explored map (see initial map)."),
"default": () => false,
"defined": () => g_GameAttributes.settings.ExploreMap !== undefined,
"get": () => g_GameAttributes.settings.ExploreMap,
"set": checked => {
g_GameAttributes.settings.ExploreMap = checked;
},
"enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RevealMap,
"initOrder": 1000
},
"disableTreasures": {
"title": () => translate("Disable Treasures"),
"tooltip": () => translate("Do not add treasures to the map."),
"default": () => false,
"defined": () => g_GameAttributes.settings.DisableTreasures !== undefined,
"get": () => g_GameAttributes.settings.DisableTreasures,
"set": checked => {
g_GameAttributes.settings.DisableTreasures = checked;
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"disableSpies": {
"title": () => translate("Disable Spies"),
"tooltip": () => translate("Disable spies during the game."),
"default": () => false,
"defined": () => g_GameAttributes.settings.DisableSpies !== undefined,
"get": () => g_GameAttributes.settings.DisableSpies,
"set": checked => {
g_GameAttributes.settings.DisableSpies = checked;
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"lockTeams": {
"title": () => translate("Teams Locked"),
"tooltip": () => translate("Toggle locked teams."),
"default": () => Engine.HasXmppClient(),
"defined": () => g_GameAttributes.settings.LockTeams !== undefined,
"get": () => g_GameAttributes.settings.LockTeams,
"set": checked => {
g_GameAttributes.settings.LockTeams = checked;
g_GameAttributes.settings.LastManStanding = false;
},
"enabled": () =>
g_GameAttributes.mapType != "scenario" &&
!g_GameAttributes.settings.RatingEnabled,
"initOrder": 1000
},
"lastManStanding": {
"title": () => translate("Last Man Standing"),
"tooltip": () => translate("Toggle whether the last remaining player or the last remaining set of allies wins."),
"default": () => false,
"defined": () => g_GameAttributes.settings.LastManStanding !== undefined,
"get": () => g_GameAttributes.settings.LastManStanding,
"set": checked => {
g_GameAttributes.settings.LastManStanding = checked;
},
"enabled": () =>
g_GameAttributes.mapType != "scenario" &&
!g_GameAttributes.settings.LockTeams,
"initOrder": 1000
},
"enableCheats": {
"title": () => translate("Cheats"),
"tooltip": () => translate("Toggle the usability of cheats."),
"default": () => !g_IsNetworked,
"hidden": () => !g_IsNetworked,
"defined": () => g_GameAttributes.settings.CheatsEnabled !== undefined,
"get": () => g_GameAttributes.settings.CheatsEnabled,
"set": checked => {
g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked ||
checked && !g_GameAttributes.settings.RatingEnabled;
},
"enabled": () => !g_GameAttributes.settings.RatingEnabled,
"initOrder": 1000
},
"enableRating": {
"title": () => translate("Rated Game"),
"tooltip": () => translate("Toggle if this game will be rated for the leaderboard."),
"default": () => Engine.HasXmppClient(),
"hidden": () => !Engine.HasXmppClient(),
"defined": () => g_GameAttributes.settings.RatingEnabled !== undefined,
"get": () => !!g_GameAttributes.settings.RatingEnabled,
"set": checked => {
g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient() ? checked : undefined;
Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
if (checked)
{
g_Checkboxes.lockTeams.set(true);
g_Checkboxes.enableCheats.set(false);
}
},
"initOrder": 1000
},
}
);
/**
* For setting up arbitrary GUI objects.
*/
var g_MiscControls = {
"chatPanel": {
"hidden": () => {
if (!g_IsNetworked)
return true;
let size = Engine.GetGUIObjectByName("chatPanel").getComputedSize();
return size.right - size.left < g_MinChatWidth;
},
},
"chatInput": {
"tooltip": () => colorizeAutocompleteHotkey(translate("Press %(hotkey)s to autocomplete player names or settings.")),
},
"cheatWarningText": {
"hidden": () => !g_IsNetworked || !g_GameAttributes.settings.CheatsEnabled,
},
"cancelGame": {
"tooltip": () =>
Engine.HasXmppClient() ?
translate("Return to the lobby.") :
translate("Return to the main menu."),
},
"startGame": {
"caption": () =>
g_IsController ? translate("Start Game!") : g_ReadyData[g_IsReady].caption,
"onPress": () => function() {
if (g_IsController)
launchGame();
else
toggleReady();
},
"onPressRight": () => function() {
if (!g_IsController && g_IsReady)
setReady(0, true);
},
"tooltip": (hoverIdx) =>
!g_IsController ?
g_ReadyData[g_IsReady].tooltip :
!g_IsNetworked || Object.keys(g_PlayerAssignments).every(guid =>
g_PlayerAssignments[guid].status || g_PlayerAssignments[guid].player == -1) ?
translate("Start a new game with the current settings.") :
translate("Start a new game with the current settings (disabled until all players are ready)."),
"enabled": () => !g_GameStarted && (
!g_IsController ||
Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].status ||
g_PlayerAssignments[guid].player == -1 ||
guid == Engine.GetPlayerGUID() && g_IsController)),
"hidden": () =>
!g_PlayerAssignments[Engine.GetPlayerGUID()] ||
g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1 && !g_IsController,
},
"civInfoButton": {
"tooltip": () => sprintf(
translate("%(hotkey_civinfo)s / %(hotkey_structree)s: View History / Structure Tree\nLast opened will be reopened on click."), {
"hotkey_civinfo": colorizeHotkey("%(hotkey)s", "civinfo"),
"hotkey_structree": colorizeHotkey("%(hotkey)s", "structree")
}),
"onPress": () => function() {
- Engine.PushGuiPage(g_CivInfo.page, {
- "civ": g_CivInfo.code,
- "callback": "storeCivInfoPage"
- });
+ Engine.PushGuiPage(
+ g_CivInfo.page,
+ { "civ": g_CivInfo.civ },
+ storeCivInfoPage);
}
},
"civResetButton": {
"hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
},
"teamResetButton": {
"hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
},
"lobbyButton": {
"onPress": () => function() {
if (Engine.HasXmppClient())
Engine.PushGuiPage("page_lobby.xml", { "dialog": true });
},
"hidden": () => !Engine.HasXmppClient()
},
"spTips": {
"hidden": () => {
let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
let spTips = Engine.GetGUIObjectByName("spTips");
return g_IsNetworked ||
Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true" ||
spTips.size.right > settingsPanel.getComputedSize().left;
}
}
};
/**
* Contains gui elements that are repeated for every player.
*/
var g_PlayerMiscElements = {
"playerBox": {
"size": (playerIdx) => ["0", 32 * playerIdx, "100%", 32 * (playerIdx + 1)].join(" "),
},
"playerName": {
"caption": (playerIdx) => {
let pData = g_GameAttributes.settings.PlayerData[playerIdx];
let assignedGUID = Object.keys(g_PlayerAssignments).find(
guid => g_PlayerAssignments[guid].player == playerIdx + 1);
let name = translate(pData.Name || g_DefaultPlayerData[playerIdx].Name);
if (g_IsNetworked)
name = coloredText(name, g_ReadyData[assignedGUID ? g_PlayerAssignments[assignedGUID].status : 2].color);
return name;
},
},
"playerColor": {
"sprite": (playerIdx) => "color:" + rgbToGuiColor(g_GameAttributes.settings.PlayerData[playerIdx].Color, 100),
},
"playerConfig": {
"hidden": (playerIdx) => !g_GameAttributes.settings.PlayerData[playerIdx].AI,
"onPress": (playerIdx) => function() {
openAIConfig(playerIdx);
},
"tooltip": (playerIdx) => sprintf(translate("Configure AI: %(description)s."), {
"description": translateAISettings(g_GameAttributes.settings.PlayerData[playerIdx])
}),
},
};
/**
* Initializes some globals without touching the GUI.
*
* @param {Object} attribs - context data sent by the lobby / mainmenu
*/
function init(attribs)
{
if (!g_Settings)
{
cancelSetup();
return;
}
g_IsTutorial = !!attribs.tutorial;
g_ServerName = attribs.serverName;
g_ServerPort = attribs.serverPort;
g_StunEndpoint = attribs.stunEndpoint;
if (!g_IsNetworked)
g_PlayerAssignments = {
"local": {
"name": singleplayerName(),
"player": 1
}
};
// Replace empty playername when entering a singleplayermatch for the first time
if (!g_IsNetworked)
Engine.ConfigDB_CreateAndWriteValueToFile("user", "playername.singleplayer", singleplayerName(), "config/user.cfg");
initDefaults();
supplementDefaults();
setTimeout(displayGamestateNotifications, 1000);
}
function initDefaults()
{
// Remove gaia from both arrays
g_DefaultPlayerData = clone(g_Settings.PlayerDefaults.slice(1));
let aiDifficulty = +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty");
let aiBehavior = Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior");
// Don't change the underlying defaults file, as Atlas uses that file too
for (let i in g_DefaultPlayerData)
{
g_DefaultPlayerData[i].Civ = "random";
g_DefaultPlayerData[i].Team = -1;
g_DefaultPlayerData[i].AIDiff = aiDifficulty;
g_DefaultPlayerData[i].AIBehavior = aiBehavior;
}
deepfreeze(g_DefaultPlayerData);
}
/**
* Sets default values for all g_GameAttribute settings which don't have a value set.
*/
function supplementDefaults()
{
g_GameAttributes.settings.VictoryConditions = g_GameAttributes.settings.VictoryConditions ||
g_VictoryConditions.filter(victoryCondition => !!victoryCondition.Default).map(victoryCondition => victoryCondition.Name);
for (let dropdown in g_Dropdowns)
if (!g_Dropdowns[dropdown].defined())
g_Dropdowns[dropdown].select(g_Dropdowns[dropdown].default());
for (let checkbox in g_Checkboxes)
if (!g_Checkboxes[checkbox].defined())
g_Checkboxes[checkbox].set(g_Checkboxes[checkbox].default());
for (let dropdown in g_PlayerDropdowns)
for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i)
if (!isControlArrayElementHidden(i) && !g_PlayerDropdowns[dropdown].defined(i))
g_PlayerDropdowns[dropdown].select(g_PlayerDropdowns[dropdown].default(i), i);
}
/**
* Called after the first tick.
*/
function initGUIObjects()
{
initSettingObjects();
initSettingsTabButtons();
initSPTips();
loadPersistMatchSettings();
updateGameAttributes();
sendRegisterGameStanzaImmediate();
if (g_IsTutorial)
{
launchTutorial();
return;
}
// Don't lift the curtain until the controls are updated the first time
if (!g_IsNetworked)
hideLoadingWindow();
}
/**
* @param {number} dt - Time in milliseconds since last call.
*/
function slideSettingsPanel(dt)
{
let slideSpeed = Engine.ConfigDB_GetValue("user", "gui.gamesetup.settingsslide") == "true" ? g_SlideSpeed : Infinity;
let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
let rightBorder = Engine.GetGUIObjectByName("settingTabButtons").size.left;
let offset = 0;
if (g_TabCategorySelected === undefined)
{
let maxOffset = rightBorder - settingsPanel.size.left;
if (maxOffset > 0)
offset = Math.min(slideSpeed * dt, maxOffset);
}
else if (rightBorder > settingsPanel.size.right)
offset = Math.min(slideSpeed * dt, rightBorder - settingsPanel.size.right);
else
{
let maxOffset = settingsPanel.size.right - rightBorder;
if (maxOffset > 0)
offset = -Math.min(slideSpeed * dt, maxOffset);
}
updateSettingsPanelPosition(offset);
}
/**
* Directly change the position of the settingsPanel.
* @param {number} offset - Number of pixels the panel needs to move.
*/
function updateSettingsPanelPosition(offset)
{
if (!offset)
return;
let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
let settingsPanelSize = settingsPanel.size;
settingsPanelSize.left += offset;
settingsPanelSize.right += offset;
settingsPanel.size = settingsPanelSize;
let settingsBackground = Engine.GetGUIObjectByName("settingsBackground");
let backgroundSize = settingsBackground.size;
backgroundSize.left = settingsPanelSize.left;
settingsBackground.size = backgroundSize;
let chatPanel = Engine.GetGUIObjectByName("chatPanel");
let chatSize = chatPanel.size;
chatSize.right = settingsPanelSize.left - g_ChatSettingsMargin;
chatPanel.size = chatSize;
chatPanel.hidden = g_MiscControls.chatPanel.hidden();
let spTips = Engine.GetGUIObjectByName("spTips");
spTips.hidden = g_MiscControls.spTips.hidden();
}
function hideLoadingWindow()
{
let loadingWindow = Engine.GetGUIObjectByName("loadingWindow");
if (loadingWindow.hidden)
return;
loadingWindow.hidden = true;
Engine.GetGUIObjectByName("setupWindow").hidden = false;
if (!Engine.GetGUIObjectByName("chatPanel").hidden)
Engine.GetGUIObjectByName("chatInput").focus();
}
/**
* Settings under the settings tabs use a generic name.
* Player settings use custom names.
*/
function getGUIObjectNameFromSetting(setting)
{
let idxOffset = 0;
for (let category of g_SettingsTabsGUI)
{
let idx = category.settings.indexOf(setting);
if (idx != -1)
return [
"setting",
g_Dropdowns[setting] ? "Dropdown" : "Checkbox",
"[" + (idx + idxOffset) + "]"
];
idxOffset += category.settings.length;
}
// Assume there is a GUI object with exactly that setting name
return [setting, "", ""];
}
/**
* Initialize all settings dropdowns and checkboxes.
*/
function initSettingObjects()
{
// Copy all initOrder values into one object
let initOrder = {};
for (let dropdown in g_Dropdowns)
initOrder[dropdown] = g_Dropdowns[dropdown].initOrder;
for (let checkbox in g_Checkboxes)
initOrder[checkbox] = g_Checkboxes[checkbox].initOrder;
// Sort the object on initOrder so we can init the settings in an arbitrary order
for (let setting of Object.keys(initOrder).sort((a, b) => initOrder[a] - initOrder[b]))
if (g_Dropdowns[setting])
initDropdown(setting);
else if (g_Checkboxes[setting])
initCheckbox(setting);
else
warn('The setting "' + setting + '" is not defined.');
for (let dropdown in g_PlayerDropdowns)
initPlayerDropdowns(dropdown);
}
function initDropdown(name, playerIdx)
{
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
let data = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name];
let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName);
dropdown.list = data.labels(playerIdx).map((label, id) =>
data.colors && data.colors(playerIdx) ?
coloredText(label, data.colors(playerIdx)[id]) :
label);
dropdown.list_data = data.ids(playerIdx);
dropdown.onSelectionChange = function() {
if (!g_IsController ||
g_IsInGuiUpdate ||
!this.list_data[this.selected] ||
data.hidden && data.hidden(playerIdx) ||
data.enabled && !data.enabled(playerIdx))
return;
data.select(this.selected, playerIdx);
supplementDefaults();
updateGameAttributes();
};
if (data.tooltip)
dropdown.onHoverChange = function() {
this.tooltip = data.tooltip(this.hovered, playerIdx);
};
}
function initPlayerDropdowns(name)
{
for (let i = 0; i < g_MaxPlayers; ++i)
initDropdown(name, i);
}
function initCheckbox(name)
{
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
Engine.GetGUIObjectByName(guiName + guiType + guiIdx).onPress = function() {
let obj = g_Checkboxes[name];
if (!g_IsController ||
g_IsInGuiUpdate ||
obj.enabled && !obj.enabled() ||
obj.hidden && obj.hidden())
return;
obj.set(this.checked);
supplementDefaults();
updateGameAttributes();
};
}
function initSettingsTabButtons()
{
for (let tab in g_SettingsTabsGUI)
g_SettingsTabsGUI[tab].tooltip =
sprintf(translate("Toggle the %(name)s settings tab."), { "name": g_SettingsTabsGUI[tab].label }) +
colorizeHotkey("\n" + translate("Use %(hotkey)s to move a settings tab down."), "tab.next") +
colorizeHotkey("\n" + translate("Use %(hotkey)s to move a settings tab up."), "tab.prev");
let settingTabButtons = Engine.GetGUIObjectByName("settingTabButtons");
let settingTabButtonsSize = settingTabButtons.size;
settingTabButtonsSize.bottom = settingTabButtonsSize.top + g_SettingsTabsGUI.length * (g_TabButtonHeight + g_TabButtonDist);
settingTabButtonsSize.right = g_MiscControls.lobbyButton.hidden() ?
settingTabButtonsSize.right :
Engine.GetGUIObjectByName("lobbyButton").size.left - g_LobbyButtonSpacing;
settingTabButtons.size = settingTabButtonsSize;
let settingTabButtonsBackground = Engine.GetGUIObjectByName("settingTabButtonsBackground");
settingTabButtonsBackground.size = settingTabButtonsSize;
let gameDescription = Engine.GetGUIObjectByName("mapInfoDescriptionFrame");
let gameDescriptionSize = gameDescription.size;
gameDescriptionSize.top = settingTabButtonsSize.bottom + 3;
gameDescription.size = gameDescriptionSize;
if (!g_IsController)
{
g_TabCategorySelected = undefined;
updateSettingsPanelPosition(Engine.GetGUIObjectByName("settingTabButtons").size.left - Engine.GetGUIObjectByName("settingsPanel").size.left);
}
placeTabButtons(
g_SettingsTabsGUI,
g_TabButtonHeight,
g_TabButtonDist,
category => {
selectPanel(category == g_TabCategorySelected ? undefined : category);
},
() => {
updateGUIObjects();
Engine.GetGUIObjectByName("settingsPanel").hidden = false;
});
}
function initSPTips()
{
if (g_IsNetworked || Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true")
return;
Engine.GetGUIObjectByName("spTips").hidden = false;
Engine.GetGUIObjectByName("displaySPTips").checked = true;
Engine.GetGUIObjectByName("aiTips").caption = Engine.TranslateLines(Engine.ReadFile("gui/gamesetup/ai.txt"));
}
/**
* Distribute the currently visible settings over the settings panel.
* First calculate the number of columns required, then place the objects.
*/
function distributeSettings()
{
let setupWindowSize = Engine.GetGUIObjectByName("setupWindow").getComputedSize();
let columnWidth = Math.min(
g_MaxColumnWidth,
(setupWindowSize.right - setupWindowSize.left + Engine.GetGUIObjectByName("settingTabButtons").size.left) / 2);
let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
let actualSettingsPanelSize = settingsPanel.getComputedSize();
let maxPerColumn = Math.floor((actualSettingsPanelSize.bottom - actualSettingsPanelSize.top) / g_SettingHeight);
let childCount = settingsPanel.children.filter(child => !child.hidden).length;
let perColumn = childCount / Math.ceil(childCount / maxPerColumn);
let yPos = g_SettingDist;
let column = 0;
let thisColumn = 0;
let settingsPanelSize = settingsPanel.size;
for (let child of settingsPanel.children)
{
if (child.hidden)
continue;
if (thisColumn >= perColumn)
{
yPos = g_SettingDist;
++column;
thisColumn = 0;
}
let childSize = child.size;
child.size = new GUISize(
column * columnWidth,
yPos,
column * columnWidth + columnWidth - 10,
yPos + g_SettingHeight - g_SettingDist);
yPos += g_SettingHeight;
++thisColumn;
}
settingsPanelSize.right = settingsPanelSize.left + (column + 1) * columnWidth;
settingsPanel.size = settingsPanelSize;
}
/**
* Called when the client disconnects.
* The other cases from NetClient should never occur in the gamesetup.
*/
function handleNetStatusMessage(message)
{
if (message.status != "disconnected")
{
error("Unrecognised netstatus type " + message.status);
return;
}
cancelSetup();
reportDisconnect(message.reason, true);
}
/**
* Called whenever a client clicks on ready (or not ready).
*/
function handleReadyMessage(message)
{
--g_ReadyChanged;
if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1)
addChatMessage({
"type": "ready",
"status": message.status,
"guid": message.guid
});
g_PlayerAssignments[message.guid].status = message.status;
updateGUIObjects();
}
/**
* Called after every player is ready and the host decided to finally start the game.
*/
function handleGamestartMessage(message)
{
// Immediately inform the lobby server instead of waiting for the load to finish
if (g_IsController && Engine.HasXmppClient())
{
sendRegisterGameStanzaImmediate();
let clients = formatClientsForStanza();
Engine.SendChangeStateGame(clients.connectedPlayers, clients.list);
}
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"playerAssignments": g_PlayerAssignments
});
}
/**
* Called whenever the host changed any setting.
*/
function handleGamesetupMessage(message)
{
if (!message.data)
return;
g_GameAttributes = message.data;
if (!!g_GameAttributes.settings.RatingEnabled)
{
g_GameAttributes.settings.CheatsEnabled = false;
g_GameAttributes.settings.LockTeams = true;
g_GameAttributes.settings.LastManStanding = false;
}
Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
resetReadyData();
updateGUIObjects();
hideLoadingWindow();
}
/**
* Called whenever a client joins/leaves or any gamesetting is changed.
*/
function handlePlayerAssignmentMessage(message)
{
let playerChange = false;
for (let guid in message.newAssignments)
if (!g_PlayerAssignments[guid])
{
onClientJoin(guid, message.newAssignments);
playerChange = true;
}
for (let guid in g_PlayerAssignments)
if (!message.newAssignments[guid])
{
onClientLeave(guid);
playerChange = true;
}
g_PlayerAssignments = message.newAssignments;
sanitizePlayerData(g_GameAttributes.settings.PlayerData);
updateGUIObjects();
if (playerChange)
sendRegisterGameStanzaImmediate();
else
sendRegisterGameStanza();
}
function onClientJoin(newGUID, newAssignments)
{
let playername = newAssignments[newGUID].name;
addChatMessage({
"type": "connect",
"guid": newGUID,
"username": playername
});
if (newGUID != Engine.GetPlayerGUID() && Object.keys(g_PlayerAssignments).length)
soundNotification("gamesetup.join");
let isRejoiningPlayer = newAssignments[newGUID].player != -1;
// Assign the client (or only buddies if prefered) to an unused playerslot and rejoining players to their old slot
if (!isRejoiningPlayer && playername != newAssignments[Engine.GetPlayerGUID()].name)
{
let assignOption = Engine.ConfigDB_GetValue("user", "gui.gamesetup.assignplayers");
if (assignOption == "disabled" ||
assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(playername).nick) == -1)
return;
}
let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v, i) =>
Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i + 1)
);
// Client is not and cannot become assigned as player
if (!isRejoiningPlayer && freeSlot == -1)
return;
// Assign the joining client to the free slot
if (g_IsController && !isRejoiningPlayer)
Engine.AssignNetworkPlayer(freeSlot + 1, newGUID);
resetReadyData();
}
function onClientLeave(guid)
{
addChatMessage({
"type": "disconnect",
"guid": guid
});
if (g_PlayerAssignments[guid].player != -1)
resetReadyData();
}
/**
* Doesn't translate, so that lobby clients can do that locally
* (even if they don't have that map).
*/
function getMapDisplayName(map)
{
if (map == "random")
return map;
let mapData = loadMapData(map);
if (!mapData || !mapData.settings || !mapData.settings.Name)
return map;
return mapData.settings.Name;
}
function getMapPreview(map)
{
let biomePreview = g_GameAttributes.settings.Biome && getBiomePreview(map, g_GameAttributes.settings.Biome);
if (biomePreview)
return biomePreview;
let mapData = loadMapData(map);
if (!mapData || !mapData.settings || !mapData.settings.Preview)
return "nopreview.png";
return mapData.settings.Preview;
}
/**
* Filter maps with filterFunc and by chosen map type.
*
* @param {function} filterFunc - Filter function that should be applied.
* @return {Array} the maps that match the filterFunc and the chosen map type.
*/
function getFilteredMaps(filterFunc)
{
if (!g_MapPath[g_GameAttributes.mapType])
{
error("Unexpected map type: " + g_GameAttributes.mapType);
return [];
}
let maps = [];
// TODO: Should verify these are valid maps before adding to list
for (let mapFile of listFiles(g_GameAttributes.mapPath, g_GameAttributes.mapType == "random" ? ".json" : ".xml", false))
{
if (mapFile.startsWith("_"))
continue;
let file = g_GameAttributes.mapPath + mapFile;
let mapData = loadMapData(file);
if (!mapData || !mapData.settings || filterFunc && !filterFunc(mapData.settings.Keywords || []))
continue;
maps.push({
"file": file,
"name": translate(getMapDisplayName(file)),
"color": g_ColorRegular,
"description": translate(mapData.settings.Description)
});
}
return maps;
}
/**
* Initialize the dropdown containing all map filters for the selected maptype.
*/
function reloadMapFilterList()
{
g_MapFilterList = prepareForDropdown(g_MapFilters.filter(
mapFilter => getFilteredMaps(mapFilter.filter).length
));
initDropdown("mapFilter");
reloadMapList();
}
/**
* Initialize the dropdown containing all maps for the selected maptype and mapfilter.
*/
function reloadMapList()
{
let filterID = g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter);
let filterFunc = g_MapFilterList.filter[filterID];
let mapList = getFilteredMaps(filterFunc).sort(sortNameIgnoreCase);
if (g_GameAttributes.mapType == "random")
mapList.unshift({
"file": "random",
"name": translateWithContext("map selection", "Random"),
"color": g_ColorRandom,
"description": translate("Pick any of the given maps at random.")
});
g_MapSelectionList = prepareForDropdown(mapList);
initDropdown("mapSelection");
}
/**
* Initialize the dropdowns specific to each map.
*/
function reloadMapSpecific()
{
reloadBiomeList();
reloadTriggerDifficulties();
}
function reloadBiomeList()
{
let biomeList;
if (g_GameAttributes.mapType == "random" && g_GameAttributes.settings.SupportedBiomes)
{
if (typeof g_GameAttributes.settings.SupportedBiomes == "string")
biomeList = g_Settings.Biomes.filter(biome => biome.Id.startsWith(g_GameAttributes.settings.SupportedBiomes));
else
biomeList = g_Settings.Biomes.filter(
biome => g_GameAttributes.settings.SupportedBiomes.indexOf(biome.Id) != -1);
}
g_BiomeList = biomeList && prepareForDropdown(
[{
"Id": "random",
"Title": translateWithContext("biome", "Random"),
"Description": translate("Pick a biome at random."),
"Color": g_ColorRandom
}].concat(biomeList.map(biome => ({
"Id": biome.Id,
"Title": biome.Title,
"Description": biome.Description,
"Color": g_ColorRegular
}))));
initDropdown("biome");
updateGUIDropdown("biome");
}
function reloadTriggerDifficulties()
{
g_TriggerDifficultyList = undefined;
if (!g_GameAttributes.settings.SupportedTriggerDifficulties)
return;
let triggerDifficultyList;
if (g_GameAttributes.settings.SupportedTriggerDifficulties.Values === true)
triggerDifficultyList = g_Settings.TriggerDifficulties;
else
{
triggerDifficultyList = g_Settings.TriggerDifficulties.filter(
diff => g_GameAttributes.settings.SupportedTriggerDifficulties.Values.indexOf(diff.Name) != -1);
if (!triggerDifficultyList.length)
return;
}
g_TriggerDifficultyList = prepareForDropdown(
triggerDifficultyList.map(diff => ({
"Id": diff.Difficulty,
"Title": diff.Title,
"Description": diff.Tooltip,
"Default": diff.Name == g_GameAttributes.settings.SupportedTriggerDifficulties.Default
})));
initDropdown("triggerDifficulty");
updateGUIDropdown("triggerDifficulty");
}
function reloadGameSpeedChoices()
{
g_GameSpeeds = getGameSpeedChoices(Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player == -1));
initDropdown("gameSpeed");
supplementDefaults();
}
function loadMapData(name)
{
if (!name || !g_MapPath[g_GameAttributes.mapType])
return undefined;
if (name == "random")
return { "settings": { "Name": "", "Description": "" } };
if (!g_MapData[name])
g_MapData[name] = g_GameAttributes.mapType == "random" ?
Engine.ReadJSONFile(name + ".json") :
Engine.LoadMapSettings(name);
return g_MapData[name];
}
/**
* Sets the gameattributes the way they were the last time the user left the gamesetup.
*/
function loadPersistMatchSettings()
{
if (!g_IsController || Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true" || g_IsTutorial)
return;
let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP;
if (!Engine.FileExists(settingsFile))
return;
let data = Engine.ReadJSONFile(settingsFile);
if (!data || !data.attributes || !data.attributes.settings)
return;
if (data.engine_info.engine_version != Engine.GetEngineInfo().engine_version ||
!hasSameMods(data.engine_info.mods, Engine.GetEngineInfo().mods))
return;
g_IsInGuiUpdate = true;
let mapName = data.attributes.map || "";
let mapSettings = data.attributes.settings;
g_GameAttributes = data.attributes;
if (!g_IsNetworked)
mapSettings.CheatsEnabled = true;
// Replace unselectable civs with random civ
let playerData = mapSettings.PlayerData;
if (playerData && g_GameAttributes.mapType != "scenario")
for (let i in playerData)
if (!g_CivData[playerData[i].Civ] || !g_CivData[playerData[i].Civ].SelectableInGameSetup)
playerData[i].Civ = "random";
// Apply map settings
let newMapData = loadMapData(mapName);
if (newMapData && newMapData.settings)
{
for (let prop in newMapData.settings)
mapSettings[prop] = newMapData.settings[prop];
if (playerData)
mapSettings.PlayerData = playerData;
}
if (mapSettings.PlayerData)
sanitizePlayerData(mapSettings.PlayerData);
// Reload, as the maptype or mapfilter might have changed
reloadMapFilterList();
reloadMapSpecific();
g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient();
Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled);
supplementDefaults();
g_IsInGuiUpdate = false;
}
function savePersistMatchSettings()
{
if (g_IsTutorial)
return;
Engine.WriteJSONFile(
g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP,
{
"attributes":
// Delete settings if disabled, so that players are not confronted with old settings after enabling the setting again
Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ?
g_GameAttributes :
{},
"engine_info": Engine.GetEngineInfo()
});
}
function sanitizePlayerData(playerData)
{
// Remove gaia
if (playerData.length && !playerData[0])
playerData.shift();
playerData.forEach((pData, index) => {
// Use defaults if the map doesn't specify a value
for (let prop in g_DefaultPlayerData[index])
if (!(prop in pData))
pData[prop] = clone(g_DefaultPlayerData[index][prop]);
// Replace colors with the best matching color of PlayerDefaults
if (g_GameAttributes.mapType != "scenario")
{
let colorDistances = g_PlayerColorPickerList.map(color => colorDistance(color, pData.Color));
let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance)));
pData.Color = g_PlayerColorPickerList.find(color => colorDistance(color, pData.Color) == smallestDistance);
}
// If there is a player in that slot, then there can't be an AI
if (Object.keys(g_PlayerAssignments).some(guid => g_PlayerAssignments[guid].player == index + 1))
pData.AI = "";
});
ensureUniquePlayerColors(playerData);
}
function cancelSetup()
{
if (g_IsController)
savePersistMatchSettings();
Engine.DisconnectNetworkGame();
if (Engine.HasXmppClient())
{
Engine.LobbySetPlayerPresence("available");
if (g_IsController)
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false });
}
else
Engine.SwitchGuiPage("page_pregame.xml");
}
/**
* Can't init the GUI before the first tick.
* Process netmessages afterwards.
*/
function onTick()
{
if (!g_Settings)
return;
// First tick happens before first render, so don't load yet
if (g_LoadingState == 0)
++g_LoadingState;
else if (g_LoadingState == 1)
{
initGUIObjects();
++g_LoadingState;
}
else if (g_LoadingState == 2)
handleNetMessages();
updateTimers();
let now = Date.now();
let tickLength = now - g_LastTickTime;
g_LastTickTime = now;
slideSettingsPanel(tickLength);
}
/**
* Handles all pending messages sent by the net client.
*/
function handleNetMessages()
{
if (!g_IsNetworked)
return;
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);
}
}
/**
* Called when the map or the number of players changes.
*/
function unassignInvalidPlayers(maxPlayers)
{
if (g_IsNetworked)
// Remove invalid playerIDs from the servers playerassignments copy
for (let playerID = +maxPlayers + 1; playerID <= g_MaxPlayers; ++playerID)
Engine.AssignNetworkPlayer(playerID, "");
else if (g_PlayerAssignments.local.player > maxPlayers)
g_PlayerAssignments.local.player = -1;
}
function ensureUniquePlayerColors(playerData)
{
for (let i = playerData.length - 1; i >= 0; --i)
// If someone else has that color, assign an unused color
if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color)))
playerData[i].Color = g_PlayerColorPickerList.find(color => playerData.every(pData => !sameColor(color, pData.Color)));
}
function selectMap(name)
{
// Reset some map specific properties which are not necessarily redefined on each map
for (let prop of ["TriggerScripts", "CircularMap", "Garrison", "DisabledTemplates", "Biome", "SupportedBiomes", "SupportedTriggerDifficulties", "TriggerDifficulty"])
g_GameAttributes.settings[prop] = undefined;
let mapData = loadMapData(name);
let mapSettings = mapData && mapData.settings ? clone(mapData.settings) : {};
if (g_GameAttributes.mapType != "random")
delete g_GameAttributes.settings.Nomad;
if (g_GameAttributes.mapType == "scenario")
{
delete g_GameAttributes.settings.RelicDuration;
delete g_GameAttributes.settings.WonderDuration;
delete g_GameAttributes.settings.LastManStanding;
delete g_GameAttributes.settings.RegicideGarrison;
}
if (mapSettings.PlayerData)
sanitizePlayerData(mapSettings.PlayerData);
// Copy any new settings
g_GameAttributes.map = name;
g_GameAttributes.script = mapSettings.Script;
if (g_GameAttributes.map !== "random")
for (let prop in mapSettings)
g_GameAttributes.settings[prop] = mapSettings[prop];
reloadMapSpecific();
unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length);
supplementDefaults();
}
function isControlArrayElementHidden(playerIdx)
{
return playerIdx !== undefined && playerIdx >= g_GameAttributes.settings.PlayerData.length;
}
/**
* @param playerIdx - Only specified for dropdown arrays.
*/
function updateGUIDropdown(name, playerIdx = undefined)
{
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName);
let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx + idxName);
let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx + idxName);
let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx + idxName);
if (guiType == "Dropdown")
Engine.GetGUIObjectByName(guiName + "Checkbox" + guiIdx).hidden = true;
let indexHidden = isControlArrayElementHidden(playerIdx);
let obj = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name];
let hidden = indexHidden || !!obj.hidden && obj.hidden(playerIdx);
let selected = hidden ? -1 : dropdown.list_data.indexOf(String(obj.get(playerIdx)));
let enabled = !indexHidden && (!obj.enabled || obj.enabled(playerIdx));
dropdown.enabled = g_IsController && enabled;
dropdown.hidden = !g_IsController || !enabled || hidden;
dropdown.selected = selected;
dropdown.tooltip = !indexHidden && obj.tooltip ? obj.tooltip(-1, playerIdx) : "";
if (frame)
frame.hidden = hidden;
if (title && obj.title && !indexHidden)
title.caption = sprintf(translateWithContext("Title for specific setting", "%(setting)s:"), { "setting": obj.title(playerIdx) });
if (label && !indexHidden)
{
label.hidden = g_IsController && enabled || hidden;
label.caption = selected == -1 ? translateWithContext("settings value", "Unknown") : dropdown.list[selected];
}
}
/**
* Not used for the player assignments, so playerCheckboxes are not implemented,
* hence no index.
*/
function updateGUICheckbox(name)
{
let obj = g_Checkboxes[name];
let checked = obj.get();
let hidden = !!obj.hidden && obj.hidden();
let enabled = !obj.enabled || obj.enabled();
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
let checkbox = Engine.GetGUIObjectByName(guiName + guiType + guiIdx);
let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx);
let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx);
let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx);
if (guiType == "Checkbox")
Engine.GetGUIObjectByName(guiName + "Dropdown" + guiIdx).hidden = true;
checkbox.checked = checked;
checkbox.enabled = g_IsController && enabled;
checkbox.hidden = hidden || !g_IsController;
checkbox.tooltip = obj.tooltip ? obj.tooltip() : "";
label.caption = checked ? translate("Yes") : translate("No");
label.hidden = hidden || g_IsController;
if (frame)
frame.hidden = hidden;
if (title && obj.title)
title.caption = sprintf(translate("%(setting)s:"), { "setting": obj.title() });
}
function updateGUIMiscControl(name, playerIdx)
{
let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
let obj = (playerIdx === undefined ? g_MiscControls : g_PlayerMiscElements)[name];
let control = Engine.GetGUIObjectByName(name + idxName);
if (!control)
warn("No GUI object with name '" + name + "'");
let hide = isControlArrayElementHidden(playerIdx);
control.hidden = hide;
if (hide)
return;
for (let property in obj)
control[property] = obj[property](playerIdx);
}
function launchGame()
{
if (!g_IsController)
{
error("Only host can start game");
return;
}
if (!g_GameAttributes.map || g_GameStarted)
return;
// Prevent reseting the readystate or calling this function twice
g_GameStarted = true;
updateGUIMiscControl("startGame");
savePersistMatchSettings();
// Select random map
if (g_GameAttributes.map == "random")
selectMap(pickRandom(g_Dropdowns.mapSelection.ids().slice(1)));
if (g_GameAttributes.settings.Biome == "random")
g_GameAttributes.settings.Biome = pickRandom(
typeof g_GameAttributes.settings.SupportedBiomes == "string" ?
g_BiomeList.Id.slice(1).filter(biomeID => biomeID.startsWith(g_GameAttributes.settings.SupportedBiomes)) :
g_GameAttributes.settings.SupportedBiomes);
g_GameAttributes.settings.VictoryScripts = g_GameAttributes.settings.VictoryConditions.reduce(
(scripts, victoryConditionName) => scripts.concat(g_VictoryConditions[g_VictoryConditions.map(data =>
data.Name).indexOf(victoryConditionName)].Scripts.filter(script => scripts.indexOf(script) == -1)),
[]);
g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []);
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_CivData[civ].SelectableInGameSetup));
}
g_GameAttributes.settings.PlayerData[i].Civ = chosenCiv;
// Pick one of the available botnames for the chosen civ
if (g_GameAttributes.mapType === "scenario" || !g_GameAttributes.settings.PlayerData[i].AI)
continue;
let chosenName = pickRandom(g_CivData[chosenCiv].AINames);
if (!g_IsNetworked)
chosenName = translate(chosenName);
// Count how many players use the chosenName
let usedName = g_GameAttributes.settings.PlayerData.filter(pData => pData.Name && pData.Name.indexOf(chosenName) !== -1).length;
g_GameAttributes.settings.PlayerData[i].Name = !usedName ? chosenName :
sprintf(translate("%(playerName)s %(romanNumber)s"), {
"playerName": chosenName,
"romanNumber": g_RomanNumbers[usedName+1]
});
}
// Copy playernames for the purpose of replays
for (let guid in g_PlayerAssignments)
{
let player = g_PlayerAssignments[guid];
if (player.player > 0) // not observer or GAIA
g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name;
}
// Seed used for both map generation and simulation
g_GameAttributes.settings.Seed = randIntExclusive(0, Math.pow(2, 32));
g_GameAttributes.settings.AISeed = randIntExclusive(0, Math.pow(2, 32));
// Used for identifying rated game reports for the lobby
g_GameAttributes.matchID = Engine.GetMatchID();
if (g_IsNetworked)
{
Engine.SetNetworkGameAttributes(g_GameAttributes);
Engine.StartNetworkGame();
}
else
{
// Find the player ID which the user has been assigned to
let playerID = -1;
for (let i in g_GameAttributes.settings.PlayerData)
{
let assignBox = Engine.GetGUIObjectByName("playerAssignment[" + i + "]");
if (assignBox.list_data[assignBox.selected] == "guid:local")
playerID = +i + 1;
}
Engine.StartGame(g_GameAttributes, playerID);
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"playerAssignments": g_PlayerAssignments
});
}
}
function launchTutorial()
{
g_GameAttributes.mapType = "scenario";
selectMap("maps/tutorials/starting_economy_walkthrough");
launchGame();
}
/**
* Don't set any attributes here, just show the changes in the GUI.
*
* Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously.
*/
function updateGUIObjects()
{
g_IsInGuiUpdate = true;
reloadMapFilterList();
reloadMapSpecific();
reloadGameSpeedChoices();
reloadPlayerAssignmentChoices();
// Hide exceeding dropdowns and checkboxes
for (let setting of Engine.GetGUIObjectByName("settingsPanel").children)
setting.hidden = true;
// Show the relevant ones
if (g_TabCategorySelected !== undefined)
{
for (let name in g_Dropdowns)
if (g_SettingsTabsGUI[g_TabCategorySelected].settings.indexOf(name) != -1)
updateGUIDropdown(name);
for (let name in g_Checkboxes)
if (g_SettingsTabsGUI[g_TabCategorySelected].settings.indexOf(name) != -1)
updateGUICheckbox(name);
}
for (let i = 0; i < g_MaxPlayers; ++i)
{
for (let name in g_PlayerDropdowns)
updateGUIDropdown(name, i);
for (let name in g_PlayerMiscElements)
updateGUIMiscControl(name, i);
}
for (let name in g_MiscControls)
updateGUIMiscControl(name);
updateGameDescription();
distributeSettings();
rightAlignCancelButton();
updateAutocompleteEntries();
g_IsInGuiUpdate = false;
// Refresh AI config page
if (g_LastViewedAIPlayer != -1)
{
Engine.PopGuiPage();
openAIConfig(g_LastViewedAIPlayer);
}
}
function rightAlignCancelButton()
{
let offset = 10;
let startGame = Engine.GetGUIObjectByName("startGame");
let right = startGame.hidden ? startGame.size.right : startGame.size.left - offset;
let cancelGame = Engine.GetGUIObjectByName("cancelGame");
let cancelGameSize = cancelGame.size;
let buttonWidth = cancelGameSize.right - cancelGameSize.left;
cancelGameSize.right = right;
right -= buttonWidth;
for (let element of ["cheatWarningText", "onscreenToolTip"])
{
let elementSize = Engine.GetGUIObjectByName(element).size;
elementSize.right = right - (cancelGameSize.left - elementSize.right);
Engine.GetGUIObjectByName(element).size = elementSize;
}
cancelGameSize.left = right;
cancelGame.size = cancelGameSize;
}
function updateGameDescription()
{
setMapPreviewImage("mapPreview", getMapPreview(g_GameAttributes.map));
Engine.GetGUIObjectByName("mapInfoName").caption =
translateMapTitle(getMapDisplayName(g_GameAttributes.map));
Engine.GetGUIObjectByName("mapInfoDescription").caption = getGameDescription();
}
/**
* Broadcast the changed settings to all clients and the lobbybot.
*/
function updateGameAttributes()
{
if (g_IsInGuiUpdate || !g_IsController)
return;
if (g_IsNetworked)
{
Engine.SetNetworkGameAttributes(g_GameAttributes);
if (g_LoadingState >= 2)
sendRegisterGameStanza();
resetReadyData();
}
else
updateGUIObjects();
}
function openAIConfig(playerSlot)
{
g_LastViewedAIPlayer = playerSlot;
- Engine.PushGuiPage("page_aiconfig.xml", {
- "callback": "AIConfigCallback",
- "playerSlot": playerSlot,
- "id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
- "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff,
- "behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior
- });
-}
-
-/**
- * Called after closing the dialog.
- */
-function AIConfigCallback(ai)
-{
- g_LastViewedAIPlayer = -1;
-
- if (!ai.save || !g_IsController)
- return;
-
- g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
- g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
- g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior;
+ Engine.PushGuiPage(
+ "page_aiconfig.xml",
+ {
+ "playerSlot": playerSlot,
+ "id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
+ "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff,
+ "behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior
+ },
+ ai => {
+ g_LastViewedAIPlayer = -1;
+
+ if (!ai.save || !g_IsController)
+ return;
+
+ g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
+ g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
+ g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior;
- updateGameAttributes();
+ updateGameAttributes();
+ });
}
function reloadPlayerAssignmentChoices()
{
let playerChoices = sortGUIDsByPlayerID().map(guid => ({
"Choice": "guid:" + guid,
"Color": g_PlayerAssignments[guid].player == -1 ? g_PlayerAssignmentColors.observer : g_PlayerAssignmentColors.player,
"Name": g_PlayerAssignments[guid].name
}));
// Only display hidden AIs if the map preselects them
let aiChoices = g_Settings.AIDescriptions
.filter(ai => !ai.data.hidden || g_GameAttributes.settings.PlayerData.some(pData => pData.AI == ai.id))
.map(ai => ({
"Choice": "ai:" + ai.id,
"Name": sprintf(translate("AI: %(ai)s"), {
"ai": translate(ai.data.name)
}),
"Color": g_PlayerAssignmentColors.AI
}));
let unassignedSlot = [{
"Choice": "unassigned",
"Name": translate("Unassigned"),
"Color": g_PlayerAssignmentColors.unassigned
}];
g_PlayerAssignmentList = prepareForDropdown(playerChoices.concat(aiChoices).concat(unassignedSlot));
initPlayerDropdowns("playerAssignment");
}
function swapPlayers(guidToSwap, newSlot)
{
// Player slots are indexed from 0 as Gaia is omitted.
let newPlayerID = newSlot + 1;
let playerID = g_PlayerAssignments[guidToSwap].player;
// Attempt to swap the player or AI occupying the target slot,
// if any, into the slot this player is currently in.
if (playerID != -1)
{
for (let guid in g_PlayerAssignments)
{
// Move the player in the destination slot into the current slot.
if (g_PlayerAssignments[guid].player != newPlayerID)
continue;
if (g_IsNetworked)
Engine.AssignNetworkPlayer(playerID, guid);
else
g_PlayerAssignments[guid].player = playerID;
break;
}
// Transfer the AI from the target slot to the current slot.
g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI;
g_GameAttributes.settings.PlayerData[playerID - 1].AIDiff = g_GameAttributes.settings.PlayerData[newSlot].AIDiff;
g_GameAttributes.settings.PlayerData[playerID - 1].AIBehavior = g_GameAttributes.settings.PlayerData[newSlot].AIBehavior;
// Swap civilizations and colors if they aren't fixed
if (g_GameAttributes.mapType != "scenario")
{
[g_GameAttributes.settings.PlayerData[playerID - 1].Civ, g_GameAttributes.settings.PlayerData[newSlot].Civ] =
[g_GameAttributes.settings.PlayerData[newSlot].Civ, g_GameAttributes.settings.PlayerData[playerID - 1].Civ];
[g_GameAttributes.settings.PlayerData[playerID - 1].Color, g_GameAttributes.settings.PlayerData[newSlot].Color] =
[g_GameAttributes.settings.PlayerData[newSlot].Color, g_GameAttributes.settings.PlayerData[playerID - 1].Color];
}
}
if (g_IsNetworked)
Engine.AssignNetworkPlayer(newPlayerID, guidToSwap);
else
g_PlayerAssignments[guidToSwap].player = newPlayerID;
g_GameAttributes.settings.PlayerData[newSlot].AI = "";
}
function submitChatInput()
{
let chatInput = Engine.GetGUIObjectByName("chatInput");
let text = chatInput.caption;
if (!text.length)
return;
chatInput.caption = "";
if (!executeNetworkCommand(text))
Engine.SendNetworkChat(text);
chatInput.focus();
}
function senderFont(text)
{
return '[font="' + g_SenderFont + '"]' + text + '[/font]';
}
function systemMessage(message)
{
return senderFont(sprintf(translate("== %(message)s"), { "message": message }));
}
function colorizePlayernameByGUID(guid, username = "")
{
// TODO: Maybe the host should have the moderator-prefix?
if (!username)
username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player");
let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
let color = g_ColorRegular;
if (playerID > 0)
{
color = g_GameAttributes.settings.PlayerData[playerID - 1].Color;
// Enlighten playercolor to improve readability
let [h, s, l] = rgbToHsl(color.r, color.g, color.b);
let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l));
color = rgbToGuiColor({ "r": r, "g": g, "b": b });
}
return coloredText(username, color);
}
function addChatMessage(msg)
{
if (!g_FormatChatMessage[msg.type])
return;
if (msg.type == "chat")
{
let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name;
if (userName != g_PlayerAssignments[msg.guid].name &&
msg.text.toLowerCase().indexOf(splitRatingFromNick(userName).nick.toLowerCase()) != -1)
soundNotification("nick");
}
let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || "");
let text = g_FormatChatMessage[msg.type](msg, user);
if (!text)
return;
if (Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true")
text = sprintf(translate("%(time)s %(message)s"), {
"time": sprintf(translate("\\[%(time)s]"), {
"time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), translate("HH:mm"))
}),
"message": text
});
g_ChatMessages.push(text);
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}
function clearChatMessages()
{
g_ChatMessages.length = 0;
Engine.GetGUIObjectByName("chatText").caption = "";
}
function resetCivilizations()
{
for (let i in g_GameAttributes.settings.PlayerData)
g_GameAttributes.settings.PlayerData[i].Civ = "random";
updateGameAttributes();
}
function resetTeams()
{
for (let i in g_GameAttributes.settings.PlayerData)
g_GameAttributes.settings.PlayerData[i].Team = -1;
updateGameAttributes();
}
function toggleReady()
{
setReady((g_IsReady + 1) % 3, true);
}
function setReady(ready, sendMessage)
{
g_IsReady = ready;
if (sendMessage)
Engine.SendNetworkReady(g_IsReady);
updateGUIObjects();
}
function resetReadyData()
{
if (g_GameStarted)
return;
if (g_ReadyChanged < 1)
addChatMessage({ "type": "settings" });
else if (g_ReadyChanged == 2 && !g_ReadyInit)
return; // duplicate calls on init
else
g_ReadyInit = false;
g_ReadyChanged = 2;
if (!g_IsNetworked)
g_IsReady = 2;
else if (g_IsController)
{
Engine.ClearAllPlayerReady();
setReady(2, true);
}
else if (g_IsReady != 2)
setReady(0, false);
}
/**
* Send a list of playernames and distinct between players and observers.
* Don't send teams, AIs or anything else until the game was started.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
function formatClientsForStanza()
{
let connectedPlayers = 0;
let playerData = [];
for (let guid in g_PlayerAssignments)
{
let pData = { "Name": g_PlayerAssignments[guid].name };
if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
++connectedPlayers;
else
pData.Team = "observer";
playerData.push(pData);
}
return {
"list": playerDataToStringifiedTeamList(playerData),
"connectedPlayers": connectedPlayers
};
}
/**
* Send the relevant gamesettings to the lobbybot immediately.
*/
function sendRegisterGameStanzaImmediate()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
if (g_GameStanzaTimer !== undefined)
{
clearTimeout(g_GameStanzaTimer);
g_GameStanzaTimer = undefined;
}
let clients = formatClientsForStanza();
let stanza = {
"name": g_ServerName,
"port": g_ServerPort,
"hostUsername": Engine.LobbyGetNick(),
"mapName": g_GameAttributes.map,
"niceMapName": getMapDisplayName(g_GameAttributes.map),
"mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default",
"mapType": g_GameAttributes.mapType,
"victoryConditions": g_GameAttributes.settings.VictoryConditions.join(","),
"nbp": clients.connectedPlayers,
"maxnbp": g_GameAttributes.settings.PlayerData.length,
"players": clients.list,
"stunIP": g_StunEndpoint ? g_StunEndpoint.ip : "",
"stunPort": g_StunEndpoint ? g_StunEndpoint.port : "",
"mods": JSON.stringify(Engine.GetEngineInfo().mods),
};
// Only send the stanza if the relevant settings actually changed
if (g_LastGameStanza && Object.keys(stanza).every(prop => g_LastGameStanza[prop] == stanza[prop]))
return;
g_LastGameStanza = stanza;
Engine.SendRegisterGame(stanza);
}
/**
* Send the relevant gamesettings to the lobbybot in a deferred manner.
*/
function sendRegisterGameStanza()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
if (g_GameStanzaTimer !== undefined)
clearTimeout(g_GameStanzaTimer);
g_GameStanzaTimer = setTimeout(sendRegisterGameStanzaImmediate, g_GameStanzaTimeout * 1000);
}
/**
* Figures out all strings that can be autocompleted and sorts
* them by priority (so that playernames are always autocompleted first).
*/
function updateAutocompleteEntries()
{
let autocomplete = { "0": [] };
for (let control of [g_Dropdowns, g_Checkboxes])
for (let name in control)
autocomplete[0] = autocomplete[0].concat(control[name].title());
for (let dropdown of [g_Dropdowns, g_PlayerDropdowns])
for (let name in dropdown)
{
let priority = dropdown[name].autocomplete;
if (priority === undefined)
continue;
autocomplete[priority] = (autocomplete[priority] || []).concat(dropdown[name].labels());
}
g_Autocomplete = Object.keys(autocomplete).sort().reverse().reduce((all, priority) => all.concat(autocomplete[priority]), []);
}
function storeCivInfoPage(data)
{
- g_CivInfo.code = data.civ;
- g_CivInfo.page = data.page;
+ if (data.nextPage)
+ Engine.PushGuiPage(
+ data.nextPage,
+ { "civ": data.civ },
+ storeCivInfoPage);
+ else
+ g_CivInfo = data;
}
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.xml (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.xml (revision 22676)
@@ -1,259 +1,255 @@
Index: ps/trunk/binaries/data/mods/public/gui/locale/locale.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/locale/locale.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/locale/locale.js (revision 22676)
@@ -1,74 +1,74 @@
function init()
{
var languageList = Engine.GetGUIObjectByName("languageList");
languageList.list = Engine.GetSupportedLocaleDisplayNames();
languageList.list_data = Engine.GetSupportedLocaleBaseNames();
var currentLocale = Engine.GetCurrentLocale();
var currentLocaleDictName = Engine.GetFallbackToAvailableDictLocale(currentLocale);
var useLongStrings = Engine.UseLongStrings();
var index = -1;
if (useLongStrings)
index = languageList.list_data.indexOf("long");
if (index == -1)
index = languageList.list_data.indexOf(currentLocaleDictName);
if (index != -1)
languageList.selected = index;
var localeText = Engine.GetGUIObjectByName("localeText");
if (useLongStrings)
localeText.caption = "long";
else
localeText.caption = currentLocale;
}
function cancelSetup()
{
Engine.PopGuiPage();
}
function applySelectedLocale()
{
var localeText = Engine.GetGUIObjectByName("localeText");
if(!Engine.SaveLocale(localeText.caption))
{
warn("Selected locale could not be saved in the configuration!");
return;
}
Engine.ReevaluateCurrentLocaleAndReload();
Engine.SwitchGuiPage("page_pregame.xml");
}
function languageSelectionChanged()
{
var languageList = Engine.GetGUIObjectByName("languageList");
var locale = languageList.list_data[languageList.selected];
if (locale == "long")
warn("'long' is not an actual language, just a collection of all longest strings extracted from some languages");
else if(!Engine.ValidateLocale(locale))
warn("Selected locale is not valid! This is not expected, please report the issue.");
var localeText = Engine.GetGUIObjectByName("localeText");
localeText.caption = locale;
}
function openAdvancedMenu()
{
let localeText = Engine.GetGUIObjectByName("localeText");
- Engine.PushGuiPage("page_locale_advanced.xml", { "callback": "applyFromAdvancedMenu", "locale": localeText.caption });
+ Engine.PushGuiPage("page_locale_advanced.xml", { "locale": localeText.caption }, applyFromAdvancedMenu);
}
function applyFromAdvancedMenu(locale)
{
var languageList = Engine.GetGUIObjectByName("languageList");
var currentLocaleDictName = Engine.GetFallbackToAvailableDictLocale(locale);
var index = -1;
index = languageList.list_data.indexOf(currentLocaleDictName);
if (index != -1)
languageList.selected = index;
var localeText = Engine.GetGUIObjectByName("localeText");
localeText.caption = locale;
}
Index: ps/trunk/binaries/data/mods/public/gui/locale_advanced/locale_advanced.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/locale_advanced/locale_advanced.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/locale_advanced/locale_advanced.js (revision 22676)
@@ -1,118 +1,118 @@
function init(initData)
{
let languageList = Engine.GetGUIObjectByName("languageList");
let countryList = Engine.GetGUIObjectByName("countryList");
let resultingLocaleText = Engine.GetGUIObjectByName("resultingLocale");
let scriptInput = Engine.GetGUIObjectByName("scriptInput");
// get languageList data. Only list languages for which we have a dictionary.
let languageListData = [];
let languageListTmp = Engine.GetSupportedLocaleBaseNames();
let currentLocaleLanguage = Engine.GetLocaleLanguage(initData.locale);
for (let i = 0; i < languageListTmp.length; ++i)
{
let lang = Engine.GetLocaleLanguage(languageListTmp[i]);
if (lang != "" && languageListData.indexOf(lang) == -1)
languageListData.push(lang);
}
// get countryList data (we get all countries and not only the ones we have dictionaries for)
var countryListData = [];
countryListData.push(translateWithContext("localeCountry", "None"));
var countryListTmp = Engine.GetAllLocales();
var currentLocaleCountry = Engine.GetLocaleCountry(initData.locale);
for (let i = 0; i < countryListTmp.length; ++i)
{
let country = Engine.GetLocaleCountry(countryListTmp[i]);
if (country != "" && countryListData.indexOf(country) == -1)
countryListData.push(country);
}
countryListData.sort();
// fill the languageList
languageList.list = languageListData;
languageList.list_data = languageListData;
if (languageList.list_data.indexOf(currentLocaleLanguage) != -1)
languageList.selected = languageList.list_data.indexOf(currentLocaleLanguage);
// fill the country list
countryList.list = countryListData;
countryList.list_data = countryListData;
if (currentLocaleCountry != "")
countryList.selected = countryList.list_data.indexOf(currentLocaleCountry);
else
countryList.selected = 0;
// fill the script
scriptInput.caption = Engine.GetLocaleScript(initData.locale);
}
// TODO: an onChanged event for input boxes would be useful and would allow us to avoid a tick event here.
function onTick()
{
updateResultingLocale();
}
function cancelSetup()
{
Engine.PopGuiPage();
}
function updateResultingLocale()
{
var languageList = Engine.GetGUIObjectByName("languageList");
var countryList = Engine.GetGUIObjectByName("countryList");
var resultingLocaleText = Engine.GetGUIObjectByName("resultingLocale");
var scriptInput = Engine.GetGUIObjectByName("scriptInput");
var variantInput = Engine.GetGUIObjectByName("variantInput");
var dictionaryFile = Engine.GetGUIObjectByName("dictionaryFile");
var resultingLocaleTmp = languageList.list_data[languageList.selected];
if (scriptInput.caption != "")
resultingLocaleTmp = resultingLocaleTmp + "_" + scriptInput.caption;
if (countryList.selected != -1 && countryList.list_data[countryList.selected] != translateWithContext("localeCountry", "None"))
resultingLocaleTmp = resultingLocaleTmp + "_" + countryList.list_data[countryList.selected];
let acceptButton = Engine.GetGUIObjectByName("acceptButton");
if (Engine.ValidateLocale(resultingLocaleTmp))
{
resultingLocaleText.caption = resultingLocaleTmp;
let dictionaryFileList = Engine.GetDictionariesForLocale(Engine.GetDictionaryLocale(resultingLocaleTmp));
let dictionaryFileString = "";
dictionaryFileList.forEach(entry => { dictionaryFileString = dictionaryFileString + entry + "\n"; });
dictionaryFile.caption = dictionaryFileString;
acceptButton.enabled = true;
}
else
{
resultingLocaleText.caption = translate("invalid locale");
dictionaryFile.caption = "";
acceptButton.enabled = false;
}
}
function autoDetectLocale()
{
var languageList = Engine.GetGUIObjectByName("languageList");
var countryList = Engine.GetGUIObjectByName("countryList");
var scriptInput = Engine.GetGUIObjectByName("scriptInput");
var variantInput = Engine.GetGUIObjectByName("variantInput");
var dictionaryFile = Engine.GetGUIObjectByName("dictionaryFile");
variantInput.caption = "";
dictionaryFile.caption = "";
var locale = Engine.GetDictionaryLocale("");
languageList.selected = languageList.list_data.indexOf(Engine.GetLocaleLanguage(locale));
countryList.selected = countryList.list_data.indexOf(Engine.GetLocaleCountry(locale));
scriptInput.caption = Engine.GetLocaleScript(locale);
}
function applySelectedLocale()
{
var resultingLocaleText = Engine.GetGUIObjectByName("resultingLocale");
- Engine.PopGuiPageCB(resultingLocaleText.caption);
+ Engine.PopGuiPage(resultingLocaleText.caption);
}
Index: ps/trunk/binaries/data/mods/public/gui/manual/manual.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/manual/manual.xml (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/manual/manual.xml (revision 22676)
@@ -1,30 +1,33 @@
Manual
-
+
+ this.caption = Engine.TranslateLines(Engine.ReadFile("gui/manual/intro.txt"));
+
Close
- closeManual();
+ Engine.PopGuiPage();
+
View Online
openURL("https://trac.wildfiregames.com/wiki/0adManual");
Index: ps/trunk/binaries/data/mods/public/gui/options/options.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/options/options.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/options/options.js (revision 22676)
@@ -1,389 +1,380 @@
/**
* Translated JSON file contents.
*/
var g_Options;
/**
- * Remember whether to unpause running singleplayer games.
- */
-var g_HasCallback;
-
-/**
- * Functions to call after closing the page.
+ * Names of session functions to be called after closing the page.
*/
var g_CloseCallbacks;
/**
* Vertical size of a tab button.
*/
var g_TabButtonHeight = 30;
/**
* Vertical space between two tab buttons.
*/
var g_TabButtonDist = 5;
/**
* Vertical distance between the top of the page and the first option.
*/
var g_OptionControlOffset = 5;
/**
* Vertical size of each option control.
*/
var g_OptionControlHeight = 26;
/**
* Vertical distance between two consecutive options.
*/
var g_OptionControlDist = 2;
/**
* Horizontal indentation to distinguish options that depend on another option.
*/
var g_DependentLabelIndentation = 25;
/**
* Color used to indicate that the string entered by the player isn't a sane color.
*/
var g_InsaneColor = "255 0 255";
/**
* Defines the parsing of config strings and GUI control interaction for the different option types.
*
* @property configToValue - parses a string from the user config to a value of the declared type.
* @property valueToGui - sets the GUI control to display the given value.
* @property guiToValue - returns the value of the GUI control.
* @property guiSetter - event name that should be considered a value change of the GUI control.
* @property initGUI - sets properties of the GUI control that are independent of the current value.
* @property sanitizeValue - Displays a visual clue if the entered value is invalid and returns a sane value.
* @property tooltip - appends a custom tooltip to the given option description depending on the current value.
*/
var g_OptionType = {
"boolean":
{
"configToValue": config => config == "true",
"valueToGui": (value, control) => {
control.checked = value;
},
"guiToValue": control => control.checked,
"guiSetter": "onPress"
},
"string":
{
"configToValue": value => value,
"valueToGui": (value, control) => {
control.caption = value;
},
"guiToValue": control => control.caption,
"guiSetter": "onTextEdit"
},
"color":
{
"configToValue": value => value,
"valueToGui": (value, control) => {
control.caption = value;
},
"guiToValue": control => control.caption,
"guiSetter": "onTextEdit",
"sanitizeValue": (value, control, option) => {
let color = guiToRgbColor(value);
let sanitized = rgbToGuiColor(color);
if (control)
{
control.sprite = sanitized == value ? "ModernDarkBoxWhite" : "ModernDarkBoxWhiteInvalid";
control.children[1].sprite = sanitized == value ? "color:" + value : "color:" + g_InsaneColor;
}
return sanitized;
},
"tooltip": (value, option) =>
sprintf(translate("Default: %(value)s"), {
"value": Engine.ConfigDB_GetValue("default", option.config)
})
},
"number":
{
"configToValue": value => value,
"valueToGui": (value, control) => {
control.caption = value;
},
"guiToValue": control => control.caption,
"guiSetter": "onTextEdit",
"sanitizeValue": (value, control, option) => {
let sanitized =
Math.min(option.max !== undefined ? option.max : +Infinity,
Math.max(option.min !== undefined ? option.min : -Infinity,
isNaN(+value) ? 0 : value));
if (control)
control.sprite = sanitized == value ? "ModernDarkBoxWhite" : "ModernDarkBoxWhiteInvalid";
return sanitized;
},
"tooltip": (value, option) =>
sprintf(
option.min !== undefined && option.max !== undefined ?
translateWithContext("option number", "Min: %(min)s, Max: %(max)s") :
option.min !== undefined && option.max === undefined ?
translateWithContext("option number", "Min: %(min)s") :
option.min === undefined && option.max !== undefined ?
translateWithContext("option number", "Max: %(max)s") :
"",
{
"min": option.min,
"max": option.max
})
},
"dropdown":
{
"configToValue": value => value,
"valueToGui": (value, control) => {
control.selected = control.list_data.indexOf(value);
},
"guiToValue": control => control.list_data[control.selected],
"guiSetter": "onSelectionChange",
"initGUI": (option, control) => {
control.list = option.list.map(e => e.label);
control.list_data = option.list.map(e => e.value);
},
},
"slider":
{
"configToValue": value => +value,
"valueToGui": (value, control) => {
control.value = +value;
},
"guiToValue": control => control.value,
"guiSetter": "onValueChange",
"initGUI": (option, control) => {
control.max_value = option.max;
control.min_value = option.min;
},
"tooltip": (value, option) =>
sprintf(translateWithContext("slider number", "Value: %(val)s (min: %(min)s, max: %(max)s)"), {
"val": value.toFixed(2),
"min": option.min.toFixed(2),
"max": option.max.toFixed(2)
})
}
};
function init(data, hotloadData)
{
- g_CloseCallbacks = new Set();
- g_HasCallback = hotloadData && hotloadData.callback || data && data.callback;
+ g_CloseCallbacks = hotloadData ? hotloadData.closeCallbacks : new Set();
g_TabCategorySelected = hotloadData ? hotloadData.tabCategorySelected : 0;
g_Options = Engine.ReadJSONFile("gui/options/options.json");
translateObjectKeys(g_Options, ["label", "tooltip"]);
deepfreeze(g_Options);
placeTabButtons(
g_Options,
g_TabButtonHeight,
g_TabButtonDist,
selectPanel,
displayOptions);
}
function getHotloadData()
{
return {
"tabCategorySelected": g_TabCategorySelected,
- "callback": g_HasCallback
+ "closeCallbacks": g_CloseCallbacks
};
}
/**
* Sets up labels and controls of all options of the currently selected category.
*/
function displayOptions()
{
// Hide all controls
for (let body of Engine.GetGUIObjectByName("option_controls").children)
{
body.hidden = true;
for (let control of body.children)
control.hidden = true;
}
// Initialize label and control of each option for this category
for (let i = 0; i < g_Options[g_TabCategorySelected].options.length; ++i)
{
// Position vertically
let body = Engine.GetGUIObjectByName("option_control[" + i + "]");
let bodySize = body.size;
bodySize.top = g_OptionControlOffset + i * (g_OptionControlHeight + g_OptionControlDist);
bodySize.bottom = bodySize.top + g_OptionControlHeight;
body.size = bodySize;
body.hidden = false;
// Load option data
let option = g_Options[g_TabCategorySelected].options[i];
let optionType = g_OptionType[option.type];
let value = optionType.configToValue(Engine.ConfigDB_GetValue("user", option.config));
// Setup control
let control = Engine.GetGUIObjectByName("option_control_" + option.type + "[" + i + "]");
control.tooltip = option.tooltip + (optionType.tooltip ? "\n" + optionType.tooltip(value, option) : "");
control.hidden = false;
if (optionType.initGUI)
optionType.initGUI(option, control);
control[optionType.guiSetter] = function() {};
optionType.valueToGui(value, control);
if (optionType.sanitizeValue)
optionType.sanitizeValue(value, control, option);
control[optionType.guiSetter] = function() {
let value = optionType.guiToValue(control);
if (optionType.sanitizeValue)
optionType.sanitizeValue(value, control, option);
control.tooltip = option.tooltip + (optionType.tooltip ? "\n" + optionType.tooltip(value, option) : "");
Engine.ConfigDB_CreateValue("user", option.config, String(value));
Engine.ConfigDB_SetChanges("user", true);
if (option.function)
Engine[option.function](value);
if (option.callback)
g_CloseCallbacks.add(option.callback);
enableButtons();
};
// Setup label
let label = Engine.GetGUIObjectByName("option_label[" + i + "]");
label.caption = option.label;
label.tooltip = option.tooltip;
label.hidden = false;
let labelSize = label.size;
labelSize.left = option.dependencies ? g_DependentLabelIndentation : 0;
labelSize.rright = control.size.rleft;
label.size = labelSize;
}
enableButtons();
}
/**
* Enable exactly the buttons whose dependencies are met.
*/
function enableButtons()
{
g_Options[g_TabCategorySelected].options.forEach((option, i) => {
let enabled =
!option.dependencies ||
option.dependencies.every(config => Engine.ConfigDB_GetValue("user", config) == "true");
Engine.GetGUIObjectByName("option_label[" + i + "]").enabled = enabled;
Engine.GetGUIObjectByName("option_control_" + option.type + "[" + i + "]").enabled = enabled;
});
let hasChanges = Engine.ConfigDB_HasChanges("user");
Engine.GetGUIObjectByName("revertChanges").enabled = hasChanges;
Engine.GetGUIObjectByName("saveChanges").enabled = hasChanges;
}
function setDefaults()
{
messageBox(
500, 200,
translate("Resetting the options will erase your saved settings. Do you want to continue?"),
translate("Warning"),
[translate("No"), translate("Yes")],
[null, reallySetDefaults]
);
}
function reallySetDefaults()
{
for (let category in g_Options)
for (let option of g_Options[category].options)
Engine.ConfigDB_RemoveValue("user", option.config);
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
revertChanges();
}
function revertChanges()
{
Engine.ConfigDB_Reload("user");
Engine.ConfigDB_SetChanges("user", false);
for (let category in g_Options)
for (let option of g_Options[category].options)
if (option.function)
Engine[option.function](
g_OptionType[option.type].configToValue(
Engine.ConfigDB_GetValue("user", option.config)));
displayOptions();
}
function saveChanges()
{
for (let category in g_Options)
for (let i = 0; i < g_Options[category].options.length; ++i)
{
let option = g_Options[category].options[i];
let optionType = g_OptionType[option.type];
if (!optionType.sanitizeValue)
continue;
let value = optionType.configToValue(Engine.ConfigDB_GetValue("user", option.config));
if (value == optionType.sanitizeValue(value, undefined, option))
continue;
selectPanel(category);
messageBox(
500, 200,
translate("Some setting values are invalid! Are you sure to save them?"),
translate("Warning"),
[translate("No"), translate("Yes")],
[null, reallySaveChanges]
);
return;
}
reallySaveChanges();
}
function reallySaveChanges()
{
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
Engine.ConfigDB_SetChanges("user", false);
enableButtons();
}
/**
* Close GUI page and call callbacks if they exist.
**/
function closePage()
{
if (Engine.ConfigDB_HasChanges("user"))
messageBox(
500, 200,
translate("You have unsaved changes, do you want to close this window?"),
translate("Warning"),
[translate("No"), translate("Yes")],
[null, closePageWithoutConfirmation]);
else
closePageWithoutConfirmation();
}
function closePageWithoutConfirmation()
{
- if (g_HasCallback)
- Engine.PopGuiPageCB(g_CloseCallbacks);
- else
- Engine.PopGuiPage();
+ Engine.PopGuiPage(g_CloseCallbacks);
}
Index: ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.js (revision 22676)
@@ -1,234 +1,244 @@
var currentSubmenuType; // contains submenu type
var MARGIN = 4; // menu border size
var g_ShowSplashScreens;
/**
* Available backdrops
*/
var g_BackgroundLayerData = [];
/**
* Chosen backdrop
*/
var g_BackgroundLayerset;
var g_T0 = Date.now();
var g_LastTickTime = Date.now();
function init(initData, hotloadData)
{
initMusic();
global.music.setState(global.music.states.MENU);
// Initialize currentSubmenuType with placeholder to avoid null when switching
currentSubmenuType = "submenuSinglePlayer";
// Only show splash screen(s) once at startup, but not again after hotloading
g_ShowSplashScreens = hotloadData ? hotloadData.showSplashScreens : initData && initData.isStartup;
// Pick a random background and initialise it
g_BackgroundLayerset = pickRandom(g_BackgroundLayerData);
for (let i = 0; i < g_BackgroundLayerset.length; ++i)
{
let guiObj = Engine.GetGUIObjectByName("background[" + i + "]");
guiObj.hidden = false;
guiObj.sprite = g_BackgroundLayerset[i].sprite;
guiObj.z = i;
}
Engine.GetGUIObjectByName("structreeButton").tooltip = colorizeHotkey(
translate("%(hotkey)s: View the structure tree of civilizations featured in 0 A.D."),
"structree");
Engine.GetGUIObjectByName("civInfoButton").tooltip = colorizeHotkey(
translate("%(hotkey)s: Learn about the many civilizations featured in 0 A.D."),
"civinfo");
Engine.GetGUIObjectByName("lobbyButton").tooltip = colorizeHotkey(
translate("%(hotkey)s: Launch the multiplayer lobby to join and host publicly visible games and chat with other players."),
"lobby");
}
function getHotloadData()
{
return { "showSplashScreens": g_ShowSplashScreens };
}
function scrollBackgrounds()
{
for (let i = 0; i < g_BackgroundLayerset.length; ++i)
{
let guiObj = Engine.GetGUIObjectByName("background[" + i + "]");
let screen = guiObj.parent.getComputedSize();
let h = screen.bottom - screen.top;
let w = h * 16/9;
let iw = h * 2;
let offset = g_BackgroundLayerset[i].offset((Date.now() - g_T0) / 1000, w);
if (g_BackgroundLayerset[i].tiling)
{
let left = offset % iw;
if (left >= 0)
left -= iw;
guiObj.size = new GUISize(left, screen.top, screen.right, screen.bottom);
}
else
guiObj.size = new GUISize(screen.right/2 - h + offset, screen.top, screen.right/2 + h + offset, screen.bottom);
}
}
function onTick()
{
let now = Date.now();
let tickLength = Date.now() - g_LastTickTime;
g_LastTickTime = now;
scrollBackgrounds();
updateMenuPosition(tickLength);
// Show splash screens here, so we don't interfere with main menu hotloading
if (g_ShowSplashScreens)
{
g_ShowSplashScreens = false;
if (Engine.ConfigDB_GetValue("user", "gui.splashscreen.enable") === "true" ||
Engine.ConfigDB_GetValue("user", "gui.splashscreen.version") < Engine.GetFileMTime("gui/splashscreen/splashscreen.txt"))
ShowSplashScreen();
else
ShowRenderPathMessage();
}
}
function ShowSplashScreen()
{
- Engine.PushGuiPage("page_splashscreen.xml", {
- "callback": "ShowRenderPathMessage"
- });
+ Engine.PushGuiPage("page_splashscreen.xml", {}, ShowRenderPathMessage);
}
function ShowRenderPathMessage()
{
// Warn about removing fixed render path
if (Engine.Renderer_GetRenderPath() == "fixed")
messageBox(
600, 300,
"[font=\"sans-bold-16\"]" +
sprintf(translate("%(warning)s You appear to be using non-shader (fixed function) graphics. This option will be removed in a future 0 A.D. release, to allow for more advanced graphics features. We advise upgrading your graphics card to a more recent, shader-compatible model."), {
"warning": coloredText("Warning:", "200 20 20")
}) +
"\n\n" +
// Translation: This is the second paragraph of a warning. The
// warning explains that the user is using “non-shader“ graphics,
// and that in the future this will not be supported by the game, so
// the user will need a better graphics card.
translate("Please press \"Read More\" for more information or \"OK\" to continue."),
translate("WARNING!"),
[translate("OK"), translate("Read More")],
[ null, function() { Engine.OpenURL("https://www.wildfiregames.com/forum/index.php?showtopic=16734"); } ]
);
}
/**
* Slide menu.
*/
function updateMenuPosition(dt)
{
let submenu = Engine.GetGUIObjectByName("submenu");
if (submenu.hidden == false)
{
// Number of pixels per millisecond to move
let SPEED = 1.2;
let maxOffset = Engine.GetGUIObjectByName("mainMenu").size.right - submenu.size.left;
if (maxOffset > 0)
{
let offset = Math.min(SPEED * dt, maxOffset);
let size = submenu.size;
size.left += offset;
size.right += offset;
submenu.size = size;
}
}
}
/**
* Opens the menu by revealing the screen which contains the menu.
*/
function openMenu(newSubmenu, position, buttonHeight, numButtons)
{
currentSubmenuType = newSubmenu;
Engine.GetGUIObjectByName(currentSubmenuType).hidden = false;
let submenu = Engine.GetGUIObjectByName("submenu");
let top = position - MARGIN;
let bottom = position + ((buttonHeight + MARGIN) * numButtons);
submenu.size = new GUISize(submenu.size.left, top, submenu.size.right, bottom);
// Blend in right border of main menu into the left border of the submenu
blendSubmenuIntoMain(top, bottom);
submenu.hidden = false;
}
function closeMenu()
{
Engine.GetGUIObjectByName(currentSubmenuType).hidden = true;
let submenu = Engine.GetGUIObjectByName("submenu");
submenu.hidden = true;
submenu.size = Engine.GetGUIObjectByName("mainMenu").size;
Engine.GetGUIObjectByName("MainMenuPanelRightBorderTop").size = "100%-2 0 100% 100%";
}
/**
* Sizes right border on main menu panel to match the submenu.
*/
function blendSubmenuIntoMain(topPosition, bottomPosition)
{
Engine.GetGUIObjectByName("MainMenuPanelRightBorderTop").size = "100%-2 0 100% " + (topPosition + MARGIN);
Engine.GetGUIObjectByName("MainMenuPanelRightBorderBottom").size = "100%-2 " + (bottomPosition) + " 100% 100%";
}
function exitGamePressed()
{
closeMenu();
messageBox(
400, 200,
translate("Are you sure you want to quit 0 A.D.?"),
translate("Confirmation"),
[translate("No"), translate("Yes")],
[null, Engine.Exit]
);
}
function pressedScenarioEditorButton()
{
closeMenu();
if (Engine.AtlasIsAvailable())
messageBox(
400, 200,
translate("Are you sure you want to quit 0 A.D. and open the Scenario Editor?"),
translate("Confirmation"),
[translate("No"), translate("Yes")],
[null, Engine.RestartInAtlas]
);
else
messageBox(
400, 200,
translate("The scenario editor is not available or failed to load. See the game logs for additional information."),
translate("Error")
);
}
function getLobbyDisabledByBuild()
{
return translate("Launch the multiplayer lobby to join and host publicly visible games and chat with other players. \\[DISABLED BY BUILD]");
}
+
+function openStrucTree(page)
+{
+ closeMenu();
+ Engine.PushGuiPage(page, {}, storeCivInfoPage);
+}
+
+function storeCivInfoPage(data)
+{
+ if (data.nextPage)
+ Engine.PushGuiPage(data.nextPage, { "civ": data.civ }, storeCivInfoPage);
+}
Index: ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml (revision 22676)
@@ -1,617 +1,611 @@
onTick();
closeMenu();
closeMenu();
Manual
Open the 0 A.D. Game Manual.
closeMenu();
Engine.PushGuiPage("page_manual.xml");
Tutorial
Start the economic tutorial.
Engine.SwitchGuiPage("page_gamesetup.xml", { "tutorial": true });
Structure Tree
-
- closeMenu();
- Engine.PushGuiPage("page_structree.xml", {});
-
+ openStrucTree("page_structree.xml");
History
-
- closeMenu();
- Engine.PushGuiPage("page_civinfo.xml");
-
+ openStrucTree("page_civinfo.xml");
Matches
Click here to start a new single player game.
Engine.SwitchGuiPage("page_gamesetup.xml", {});
Campaigns
Relive history through historical military campaigns. \[NOT YET IMPLEMENTED]
closeMenu();
Load Game
Click here to load a saved game.
closeMenu();
Engine.PushGuiPage("page_loadgame.xml", { "type": "offline" });
Replays
Playback previous games.
closeMenu();
Engine.SwitchGuiPage("page_replaymenu.xml", {
"replaySelectionData": {
"filters": {
"singleplayer": "Singleplayer"
}
}
});
Join Game
Joining an existing multiplayer game.
closeMenu();
Engine.PushGuiPage("page_gamesetup_mp.xml", { "multiplayerGameType": "join" });
Host Game
Host a multiplayer game.\n\nRequires UDP port 20595 to be open.
closeMenu();
Engine.PushGuiPage("page_gamesetup_mp.xml", { multiplayerGameType: "host" });
Game Lobby
if (!Engine.StartXmppClient)
return;
closeMenu();
Engine.PushGuiPage("page_prelobby_entrance.xml");
if (!Engine.StartXmppClient)
{
this.enabled = false;
this.tooltip = getLobbyDisabledByBuild();
}
Replays
Playback previous games.
closeMenu();
Engine.SwitchGuiPage("page_replaymenu.xml", {
"replaySelectionData": {
"filters": {
"singleplayer": "Multiplayer"
}
}
});
Options
Adjust game settings.
closeMenu();
Engine.PushGuiPage("page_options.xml");
Language
Choose the language of the game.
closeMenu();
Engine.PushGuiPage("page_locale.xml");
Mod Selection
Select and download mods for the game.
Engine.SwitchGuiPage("page_modmod.xml");
Welcome Screen
Show the Welcome Screen. Useful if you hid it by mistake.
closeMenu();
ShowSplashScreen();
Learn to Play
Learn how to play, start the tutorial, discover the technology trees, and the history behind the civilizations
closeMenu();
openMenu("submenuLearn", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 4);
Single Player
Challenge the computer player to a single player match.
closeMenu();
openMenu("submenuSinglePlayer", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 4);
Multiplayer
Fight against one or more human players in a multiplayer game.
closeMenu();
openMenu("submenuMultiplayer", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 4);
Settings
Game options and mod selection.
closeMenu();
openMenu("submenuOptions", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 4);
Scenario Editor
Open the Atlas Scenario Editor in a new window. You can run this more reliably by starting the game with the command-line argument "-editor".
pressedScenarioEditorButton();
Exit
Exits the game.
exitGamePressed();
[font="sans-bold-16"]
Alpha XXIV
[/font]\n\n
WARNING: This is an early development version of the game. Many features have not been added yet.
Website
Click to open play0ad.com in your web browser.
openURL("https://play0ad.com/");
Chat
Click to open the 0 A.D. IRC chat in your browser. (#0ad on webchat.quakenet.org)
openURL("https://webchat.quakenet.org/?channels=0ad");
Report a Bug
Click to visit 0 A.D. Trac to report a bug, crash, or error.
openURL("https://trac.wildfiregames.com/wiki/ReportingErrors/");
Translate the Game
Click to open the 0 A.D. translate page in your browser.
openURL("https://trac.wildfiregames.com/wiki/Localization");
Donate
Help with the project expenses by donating.
openURL("https://play0ad.com/community/donate/");
Credits
Click to see the 0 A.D. credits.
Engine.PushGuiPage("page_credits.xml");
WILDFIRE GAMES
this.caption = getBuildString();
Index: ps/trunk/binaries/data/mods/public/gui/reference/common/core.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/common/core.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/reference/common/core.js (revision 22676)
@@ -1,211 +1,202 @@
var g_SelectedCiv = "gaia";
-var g_CallbackSet = false;
-
-function closePage()
-{
- if (g_CallbackSet)
- Engine.PopGuiPageCB(0);
- else
- Engine.PopGuiPage();
-}
/**
* Compile lists of templates buildable/trainable/researchable of a given civ.
*
* @param {string} civCode - Code of the civ to get template list for. Optional,
* defaults to g_SelectedCiv.
* @return {object} containing lists of template names, grouped by type.
*/
function compileTemplateLists(civCode)
{
if (!civCode || civCode == "gaia")
return {};
let templatesToParse = [];
for (let entity of g_CivData[civCode].StartEntities)
templatesToParse.push(entity.Template);
let templateLists = {
"units": new Map(),
"structures": new Map(),
"techs": new Map(),
"wallsetPieces": new Map()
};
do {
const templatesThisIteration = templatesToParse;
templatesToParse = [];
for (let templateBeingParsed of templatesThisIteration)
{
let list = getTemplateListsFromTemplate(templateBeingParsed);
for (let type in list)
for (let templateName of list[type])
if (!templateLists[type].has(templateName))
{
templateLists[type].set(templateName, [templateBeingParsed]);
if (type != "techs")
templatesToParse.push(templateName);
}
else if (templateLists[type].get(templateName).indexOf(templateBeingParsed) == -1)
templateLists[type].get(templateName).push(templateBeingParsed);
}
} while (templatesToParse.length);
// Expand/filter tech pairs
for (let [techCode, researcherList] of templateLists.techs)
{
if (!isPairTech(techCode))
continue;
for (let subTech of loadTechnologyPair(techCode).techs)
if (!templateLists.techs.has(subTech))
templateLists.techs.set(subTech, researcherList);
else
for (let researcher of researcherList)
if (templateLists.techs.get(subTech).indexOf(researcher) == -1)
templateLists.techs.get(subTech).push(researcher);
templateLists.techs.delete(techCode);
}
// Remove wallset pieces, as they've served their purpose.
delete templateLists.wallsetPieces;
return templateLists;
}
/**
* Compiles lists of buildable, trainable, or researchable entities from
* a named template.
*/
function getTemplateListsFromTemplate(templateName)
{
if (!templateName || !Engine.TemplateExists(templateName))
return {};
// If this is a non-promotion variant (ie. {civ}_support_female_citizen_house)
// then it is functionally equivalent to another unit being processed, so skip it.
if (getBaseTemplateName(templateName) != templateName)
return {};
let template = loadTemplate(templateName);
let templateLists = loadProductionQueue(template);
templateLists.structures = loadBuildQueue(template);
if (template.WallSet)
{
templateLists.wallsetPieces = [];
for (let segment in template.WallSet.Templates)
{
segment = template.WallSet.Templates[segment].replace(/\{(civ|native)\}/g, g_SelectedCiv);
if (Engine.TemplateExists(segment))
templateLists.wallsetPieces.push(segment);
}
}
return templateLists;
}
function loadProductionQueue(template)
{
let production = {
"techs": [],
"units": []
};
if (!template.ProductionQueue)
return production;
if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string)
for (let templateName of template.ProductionQueue.Entities._string.split(" "))
{
templateName = templateName.replace(/\{(civ|native)\}/g, g_SelectedCiv);
if (Engine.TemplateExists(templateName))
production.units.push(getBaseTemplateName(templateName));
}
if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string)
for (let technologyName of template.ProductionQueue.Technologies._string.split(" "))
{
if (technologyName.indexOf("{civ}") != -1)
{
let civTechName = technologyName.replace("{civ}", g_SelectedCiv);
technologyName = techDataExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic");
}
if (isPairTech(technologyName))
for (let pairTechnologyName of loadTechnologyPair(technologyName).techs)
production.techs.push(pairTechnologyName);
else
production.techs.push(technologyName);
}
return production;
}
function loadBuildQueue(template)
{
let buildQueue = [];
if (!template.Builder || !template.Builder.Entities._string)
return buildQueue;
for (let build of template.Builder.Entities._string.split(" "))
{
build = build.replace(/\{(civ|native)\}/g, g_SelectedCiv);
if (Engine.TemplateExists(build))
buildQueue.push(build);
}
return buildQueue;
}
/**
* Returns the name of a template's base form (without `_house`, `_trireme`, or similar),
* or the template's own name if the base is of a different promotion rank.
*/
function getBaseTemplateName(templateName)
{
if (!templateName || !Engine.TemplateExists(templateName))
return undefined;
templateName = removeFiltersFromTemplateName(templateName);
let template = loadTemplate(templateName);
if (!dirname(templateName) || dirname(template["@parent"]) != dirname(templateName))
return templateName;
let parentTemplate = loadTemplate(template["@parent"]);
if (parentTemplate.Identity && parentTemplate.Identity.Rank &&
parentTemplate.Identity.Rank != template.Identity.Rank)
return templateName;
if (!parentTemplate.Cost)
return templateName;
if (parentTemplate.Upgrade)
for (let upgrade in parentTemplate.Upgrade)
if (parentTemplate.Upgrade[upgrade].Entity)
return templateName;
for (let res in parentTemplate.Cost.Resources)
if (+parentTemplate.Cost.Resources[res])
return getBaseTemplateName(template["@parent"]);
return templateName;
}
function setViewerOnPress(guiObjectName, templateName)
{
let viewerFunc = () => {
Engine.PushGuiPage("page_viewer.xml", {
"templateName": templateName,
"civ": g_SelectedCiv
});
};
Engine.GetGUIObjectByName(guiObjectName).onPress = viewerFunc;
Engine.GetGUIObjectByName(guiObjectName).onPressRight = viewerFunc;
}
Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js (revision 22676)
@@ -1,206 +1,194 @@
/**
* Array of structure template names when given a civ and a phase name.
*/
var g_BuildList = {};
/**
* Array of template names that can be trained from a unit, given a civ and unit template name.
*/
var g_TrainList = {};
/**
- * Callback function name on closing gui via Engine.PopGuiPage().
- */
-var g_Callback = "";
-
-/**
* Initialize the page
*
* @param {object} data - Parameters passed from the code that calls this page into existence.
*/
function init(data = {})
{
- if (data.callback)
- g_Callback = data.callback;
-
let civList = Object.keys(g_CivData).map(civ => ({
"name": g_CivData[civ].Name,
"code": civ,
})).sort(sortNameIgnoreCase);
if (!civList.length)
{
closePage();
return;
}
g_ParsedData = {
"units": {},
"structures": {},
"techs": {},
"phases": {}
};
let civSelection = Engine.GetGUIObjectByName("civSelection");
civSelection.list = civList.map(c => c.name);
civSelection.list_data = civList.map(c => c.code);
civSelection.selected = data.civ ? civSelection.list_data.indexOf(data.civ) : 0;
Engine.GetGUIObjectByName("civinfo").tooltip = colorizeHotkey(translate("%(hotkey)s: Switch to History."), "civinfo");
Engine.GetGUIObjectByName("close").tooltip = colorizeHotkey(translate("%(hotkey)s: Close Structure Tree."), "cancel");
}
function switchToCivInfoPage()
{
- Engine.PopGuiPage();
- Engine.PushGuiPage("page_civinfo.xml", { "civ": g_SelectedCiv, "callback": g_Callback });
+ Engine.PopGuiPage({ "civ": g_SelectedCiv, "nextPage": "page_civinfo.xml" });
}
function closePage()
{
- if (g_Callback)
- Engine.PopGuiPageCB({ "civ": g_SelectedCiv, "page": "page_structree.xml" });
- else
- Engine.PopGuiPage();
+ Engine.PopGuiPage({ "civ": g_SelectedCiv, "page": "page_structree.xml" });
}
/**
* @param {string} civCode
*/
function selectCiv(civCode)
{
if (civCode === g_SelectedCiv || !g_CivData[civCode])
return;
g_SelectedCiv = civCode;
g_CurrentModifiers = deriveModifications(g_AutoResearchTechList);
// If a buildList already exists, then this civ has already been parsed
if (g_BuildList[g_SelectedCiv])
{
draw();
drawPhaseIcons();
return;
}
let templateLists = compileTemplateLists(civCode);
for (let u of templateLists.units.keys())
if (!g_ParsedData.units[u])
g_ParsedData.units[u] = loadEntityTemplate(u);
for (let s of templateLists.structures.keys())
if (!g_ParsedData.structures[s])
g_ParsedData.structures[s] = loadEntityTemplate(s);
// Load technologies
g_ParsedData.techs[civCode] = {};
for (let techcode of templateLists.techs.keys())
if (basename(techcode).startsWith("phase"))
g_ParsedData.phases[techcode] = loadPhase(techcode);
else
g_ParsedData.techs[civCode][techcode] = loadTechnology(techcode);
// Establish phase order
g_ParsedData.phaseList = UnravelPhases(g_ParsedData.phases);
// Load any required generic phases that aren't already loaded
for (let phasecode of g_ParsedData.phaseList)
if (!g_ParsedData.phases[phasecode])
g_ParsedData.phases[phasecode] = loadPhase(phasecode);
// Group production and upgrade lists of structures by phase
for (let structCode of templateLists.structures.keys())
{
let structInfo = g_ParsedData.structures[structCode];
structInfo.phase = getPhaseOfTemplate(structInfo);
let structPhaseIdx = g_ParsedData.phaseList.indexOf(structInfo.phase);
// If this building is shared with another civ,
// it may have already gone through the grouping process already
if (!Array.isArray(structInfo.production.techs))
continue;
// Sort techs by phase
let newProdTech = {};
for (let prod of structInfo.production.techs)
{
let phase = getPhaseOfTechnology(prod);
if (phase === false)
continue;
if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx)
phase = structInfo.phase;
if (!(phase in newProdTech))
newProdTech[phase] = [];
newProdTech[phase].push(prod);
}
// Sort units by phase
let newProdUnits = {};
for (let prod of structInfo.production.units)
{
let phase = getPhaseOfTemplate(g_ParsedData.units[prod]);
if (phase === false)
continue;
if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx)
phase = structInfo.phase;
if (!(phase in newProdUnits))
newProdUnits[phase] = [];
newProdUnits[phase].push(prod);
}
g_ParsedData.structures[structCode].production = {
"techs": newProdTech,
"units": newProdUnits
};
// Sort upgrades by phase
let newUpgrades = {};
if (structInfo.upgrades)
for (let upgrade of structInfo.upgrades)
{
let phase = getPhaseOfTemplate(upgrade);
if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx)
phase = structInfo.phase;
if (!newUpgrades[phase])
newUpgrades[phase] = [];
newUpgrades[phase].push(upgrade);
}
g_ParsedData.structures[structCode].upgrades = newUpgrades;
}
// Determine the buildList for the civ (grouped by phase)
let buildList = {};
let trainerList = [];
for (let pha of g_ParsedData.phaseList)
buildList[pha] = [];
for (let structCode of templateLists.structures.keys())
{
let phase = g_ParsedData.structures[structCode].phase;
buildList[phase].push(structCode);
}
for (let unitCode of templateLists.units.keys())
{
let unitTemplate = g_ParsedData.units[unitCode];
if (!unitTemplate.production.units.length && !unitTemplate.production.techs.length && !unitTemplate.upgrades)
continue;
trainerList.push(unitCode);
}
g_BuildList[g_SelectedCiv] = buildList;
g_TrainList[g_SelectedCiv] = trainerList;
draw();
drawPhaseIcons();
}
Index: ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js (revision 22676)
@@ -1,221 +1,215 @@
/**
* Holder for the template file being displayed.
*/
var g_Template = {};
/**
* Holder for the template lists generated by compileTemplateLists().
*/
var g_TemplateLists = {};
/**
* Used to display textual information and the build/train lists of the
* template being displayed.
*
* At present, these are drawn in the main body of the page.
*/
var g_InfoFunctions = [
getEntityTooltip,
getHistoryTooltip,
getDescriptionTooltip,
getAurasTooltip,
getVisibleEntityClassesFormatted,
getBuiltByText,
getTrainedByText,
getResearchedByText,
getBuildText,
getTrainText,
getResearchText,
getUpgradeText
];
/**
* Override style so we can get a bigger specific name.
*/
g_TooltipTextFormats.nameSpecificBig.font = "sans-bold-20";
g_TooltipTextFormats.nameSpecificSmall.font = "sans-bold-16";
g_TooltipTextFormats.nameGeneric.font = "sans-bold-16";
/**
* Path to unit rank icons.
*/
var g_RankIconPath = "session/icons/ranks/";
/**
* Page initialisation. May also eventually pre-draw/arrange objects.
*
* @param {object} data - Contains the civCode and the name of the template to display.
* @param {string} data.templateName
* @param {string} [data.civ]
- * @param {*} [data.callback] - If set and loosely equivalent to true, a callback is
- * assumed to be setup ready be called by the Engine upon
- * closure of this page.
*/
function init(data)
{
if (!data || !data.templateName)
{
error("Viewer: No template provided");
closePage();
return;
}
- if (data.callback)
- g_CallbackSet = true;
-
let templateName = removeFiltersFromTemplateName(data.templateName);
let isTech = techDataExists(templateName);
// Attempt to get the civ code from the template, or, if
// it's a technology, from the researcher's template.
if (!isTech)
{
// Catch and redirect if template is a non-promotion variant of
// another (ie. units/{civ}_support_female_citizen_house).
templateName = getBaseTemplateName(templateName);
g_SelectedCiv = loadTemplate(templateName).Identity.Civ;
}
if (g_SelectedCiv == "gaia" && data.civ)
g_SelectedCiv = data.civ;
g_TemplateLists = compileTemplateLists(g_SelectedCiv);
g_CurrentModifiers = deriveModifications(g_AutoResearchTechList);
g_Template = isTech ? loadTechnology(templateName) : loadEntityTemplate(templateName);
if (!g_Template)
{
error("Viewer: unable to recognise or load template (" + templateName + ")");
closePage();
return;
}
g_StatsFunctions = [getEntityCostTooltip].concat(g_StatsFunctions);
draw();
}
/**
* Populate the UI elements.
*/
function draw()
{
Engine.GetGUIObjectByName("entityName").caption = getEntityNamesFormatted(g_Template);
let entityIcon = Engine.GetGUIObjectByName("entityIcon");
entityIcon.sprite = "stretched:session/portraits/" + g_Template.icon;
let entityStats = Engine.GetGUIObjectByName("entityStats");
entityStats.caption = buildText(g_Template, g_StatsFunctions);
let entityInfo = Engine.GetGUIObjectByName("entityInfo");
let infoSize = entityInfo.size;
// The magic '8' below provides a gap between the bottom of the icon, and the start of the info text.
infoSize.top = Math.max(entityIcon.size.bottom + 8, entityStats.size.top + entityStats.getTextSize().height);
entityInfo.size = infoSize;
entityInfo.caption = buildText(g_Template, g_InfoFunctions, "\n\n");
if (g_Template.promotion)
Engine.GetGUIObjectByName("entityRankGlyph").sprite = "stretched:" + g_RankIconPath + g_Template.promotion.current_rank + ".png";
Engine.GetGUIObjectByName("entityRankGlyph").hidden = !g_Template.promotion;
}
function getBuiltByText(template)
{
if (g_SelectedCiv == "gaia" || !g_TemplateLists.structures.has(template.name.internal))
return "";
let builders = g_TemplateLists.structures.get(template.name.internal);
if (!builders.length)
return "";
// Translation: Label before a list of the names of units that build the structure selected.
return buildListText(translate("Built by:"), builders.map(builder => getEntityNames(loadEntityTemplate(builder))));
}
function getTrainedByText(template)
{
if (g_SelectedCiv == "gaia" || !g_TemplateLists.units.has(template.name.internal))
return "";
let trainers = g_TemplateLists.units.get(template.name.internal);
if (!trainers.length)
return "";
// Translation: Label before a list of the names of structures or units that train the unit selected.
return buildListText(translate("Trained by:"), trainers.map(trainer => getEntityNames(loadEntityTemplate(trainer))));
}
function getResearchedByText(template)
{
if (g_SelectedCiv == "gaia" || !g_TemplateLists.techs.has(template.name.internal))
return "";
let researchers = g_TemplateLists.techs.get(template.name.internal);
if (!researchers.length)
return "";
// Translation: Label before a list of names of structures or units that research the technology selected.
return buildListText(translate("Researched at:"), researchers.map(researcher => getEntityNames(loadEntityTemplate(researcher))));
}
/**
* @return {string} List of the names of the buildings the selected unit can build.
*/
function getBuildText(template)
{
if (!template.builder || !template.builder.length)
return "";
// Translation: Label before a list of the names of structures the selected unit can construct or build.
return buildListText(translate("Builds:"),
template.builder.map(prod => getEntityNames(loadEntityTemplate(prod))));
}
/**
* @return {string} List of the names of the technologies the selected structure/unit can research.
*/
function getResearchText(template)
{
if (!template.production || !template.production.techs || !template.production.techs.length)
return "";
let researchNames = [];
for (let tech of template.production.techs)
{
let techTemplate = loadTechnology(tech);
if (techTemplate.reqs)
researchNames.push(getEntityNames(techTemplate));
}
// Translation: Label before a list of the names of technologies the selected unit or structure can research.
return buildListText(translate("Researches:"), researchNames);
}
/**
* @return {string} List of the names of the units the selected unit can train.
*/
function getTrainText(template)
{
if (!template.production || !template.production.units || !template.production.units.length)
return "";
// Translation: Label before a list of the names of units the selected unit or structure can train.
return buildListText(translate("Trains:"),
template.production.units.map(prod => getEntityNames(loadEntityTemplate(prod))));
}
/**
* @return {string} List of the names of the buildings/units the selected structure/unit can upgrade to.
*/
function getUpgradeText(template)
{
if (!template.upgrades)
return "";
// Translation: Label before a list of the names of units or structures the selected unit or structure can be upgradable to.
return buildListText(translate("Upgradable to:"),
template.upgrades.map(upgrade => getEntityNames(upgrade.name ? upgrade : loadEntityTemplate(upgrade.entity))));
}
Index: ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.xml (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.xml (revision 22676)
@@ -1,42 +1,42 @@
Information
Close
- closePage();
+ Engine.PopGuiPage();
Index: ps/trunk/binaries/data/mods/public/gui/savegame/save.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/savegame/save.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/savegame/save.js (revision 22676)
@@ -1,91 +1,91 @@
var g_Descriptions;
var g_SavedGameData;
function selectDescription()
{
let gameSelection = Engine.GetGUIObjectByName("gameSelection");
let gameID = gameSelection.list_data[gameSelection.selected];
Engine.GetGUIObjectByName("deleteGameButton").enabled = !!gameID;
if (!gameID)
return;
Engine.GetGUIObjectByName("saveGameDesc").caption = g_Descriptions[gameID];
}
function init(data)
{
g_SavedGameData = data && data.savedGameData || {};
let simulationState = Engine.GuiInterfaceCall("GetSimulationState");
g_SavedGameData.timeElapsed = simulationState.timeElapsed;
g_SavedGameData.states = simulationState.players.map(pState => pState.state);
let savedGames = Engine.GetSavedGames().sort(sortDecreasingDate);
let gameSelection = Engine.GetGUIObjectByName("gameSelection");
gameSelection.enabled = savedGames.length != 0;
if (!savedGames.length)
{
gameSelection.list = [translate("No saved games found")];
gameSelection.selected = -1;
return;
}
g_Descriptions = {};
for (let game of savedGames)
g_Descriptions[game.id] = game.metadata.description || "";
let engineInfo = Engine.GetEngineInfo();
gameSelection.list = savedGames.map(game => generateSavegameLabel(game.metadata, engineInfo));
gameSelection.list_data = savedGames.map(game => game.id);
gameSelection.selected = Math.min(gameSelection.selected, gameSelection.list.length - 1);
Engine.GetGUIObjectByName("deleteGameButton").tooltip = deleteTooltip();
}
function saveGame()
{
let gameSelection = Engine.GetGUIObjectByName("gameSelection");
let gameLabel = gameSelection.list[gameSelection.selected];
let gameID = gameSelection.list_data[gameSelection.selected];
let desc = Engine.GetGUIObjectByName("saveGameDesc").caption;
let name = gameID || "savegame";
if (!gameID)
{
reallySaveGame(name, desc, true);
return;
}
messageBox(
500, 200,
sprintf(translate("\"%(label)s\""), { "label": gameLabel }) + "\n" +
translate("Saved game will be permanently overwritten, are you sure?"),
translate("OVERWRITE SAVE"),
[translate("No"), translate("Yes")],
[null, function(){ reallySaveGame(name, desc, false); }]
);
}
function reallySaveGame(name, desc, nameIsPrefix)
{
if (nameIsPrefix)
Engine.SaveGamePrefix(name, desc, g_SavedGameData);
else
Engine.SaveGame(name, desc, g_SavedGameData);
closeSave();
}
function closeSave()
{
- Engine.PopGuiPageCB(0);
+ Engine.PopGuiPage();
}
// HACK: Engine.SaveGame* expects this function to be defined on the current page.
// That's why we have to pass the data to this page even if we don't need it.
function getSavedGameData()
{
return g_SavedGameData;
}
Index: ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 22676)
@@ -1,134 +1,130 @@
closeOpenDialogs();
openChat();
openChat(g_IsObserver ? "/observers" : "/allies");
openChat(g_LastChatAddressee);
toggleGUI();
toggleMenu();
toggleTutorial();
openGameSummary();
-
- Engine.PushGuiPage("page_civinfo.xml", { "civ": g_CivInfo.code, "callback": "storeCivInfoPage" });
-
+ openStrucTree("page_civinfo.xml");
-
- Engine.PushGuiPage("page_structree.xml", { "civ": g_CivInfo.code, "callback": "storeCivInfoPage" });
-
+ openStrucTree("page_structree.xml");
toggleConfigBool("silhouettes");
var newSetting = !Engine.Renderer_GetShowSkyEnabled();
Engine.Renderer_SetShowSkyEnabled(newSetting);
togglePause();
Engine.QuickSave();
Engine.QuickLoad();
performCommand(g_Selection.toList().map(ent => GetEntityState(ent)), "delete");
unloadAll();
stopUnits(g_Selection.toList());
backToWork();
updateSelectionDetails();
updateSelectionDetails();
updateSelectionDetails();
updateBarterButtons();
updateSelectionDetails();
updateBarterButtons();
findIdleUnit(g_MilitaryTypes);
findIdleUnit(["!Domestic"]);
clearSelection();
toggleRangeOverlay("Attack");
toggleRangeOverlay("Auras");
toggleRangeOverlay("Heal");
g_ShowAllStatusBars = !g_ShowAllStatusBars;
recalculateStatusBarDisplay();
Index: ps/trunk/binaries/data/mods/public/gui/session/menu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 22676)
@@ -1,1241 +1,1249 @@
// Menu / panel border size
var MARGIN = 4;
// Includes the main menu button
const NUM_BUTTONS = 10;
// Regular menu buttons
var BUTTON_HEIGHT = 32;
// The position where the bottom of the menu will end up (currently 228)
const END_MENU_POSITION = (BUTTON_HEIGHT * NUM_BUTTONS) + MARGIN;
// Menu starting position: bottom
const MENU_BOTTOM = 0;
// Menu starting position: top
const MENU_TOP = MENU_BOTTOM - END_MENU_POSITION;
// Number of pixels per millisecond to move
var MENU_SPEED = 1.2;
// Trade menu: step for probability changes
var STEP = 5;
// Shown in the trade dialog.
var g_IdleTraderTextColor = "orange";
/**
* Store civilization code and page (structree or history) opened in civilization info.
*/
var g_CivInfo = {
- "code": "",
+ "civ": "",
"page": "page_structree.xml"
};
/**
* The barter constants should match with the simulation
* Quantity of goods to sell per click.
*/
const g_BarterResourceSellQuantity = 100;
/**
* Multiplier to be applied when holding the massbarter hotkey.
*/
const g_BarterMultiplier = 5;
/**
* Barter actions, as mapped to the names of GUI Buttons.
*/
const g_BarterActions = ["Buy", "Sell"];
/**
* Currently selected resource type to sell in the barter GUI.
*/
var g_BarterSell;
var g_IsMenuOpen = false;
var g_IsDiplomacyOpen = false;
var g_IsTradeOpen = false;
var g_IsObjectivesOpen = false;
/**
* Used to disable a specific bribe button for the time we are waiting for the result of the bribe after it was clicked.
* It contains an array per viewedPlayer. This array is a list of the players that were bribed.
*/
var g_BribeButtonsWaiting = {};
/**
* Remember last viewed summary panel and charts.
*/
var g_SummarySelectedData;
// Redefined every time someone makes a tribute (so we can save some data in a closure). Called in input.js handleInputBeforeGui.
var g_FlushTributing = function() {};
function initMenu()
{
Engine.GetGUIObjectByName("menu").size = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM;
// TODO: Atlas should pass g_GameAttributes.settings
for (let button of ["menuExitButton", "summaryButton", "objectivesButton", "diplomacyButton"])
Engine.GetGUIObjectByName(button).enabled = !Engine.IsAtlasRunning();
}
function updateMenuPosition(dt)
{
let menu = Engine.GetGUIObjectByName("menu");
let maxOffset = g_IsMenuOpen ?
END_MENU_POSITION - menu.size.bottom :
menu.size.top - MENU_TOP;
if (maxOffset <= 0)
return;
let offset = Math.min(MENU_SPEED * dt, maxOffset) * (g_IsMenuOpen ? +1 : -1);
let size = menu.size;
size.top += offset;
size.bottom += offset;
menu.size = size;
}
// Opens the menu by revealing the screen which contains the menu
function openMenu()
{
g_IsMenuOpen = true;
}
// Closes the menu and resets position
function closeMenu()
{
g_IsMenuOpen = false;
}
function toggleMenu()
{
g_IsMenuOpen = !g_IsMenuOpen;
}
function optionsMenuButton()
{
closeOpenDialogs();
openOptions();
}
function lobbyDialogButton()
{
if (!Engine.HasXmppClient())
return;
closeOpenDialogs();
Engine.PushGuiPage("page_lobby.xml", { "dialog": true });
}
function chatMenuButton()
{
closeOpenDialogs();
openChat();
}
function resignMenuButton()
{
closeOpenDialogs();
pauseGame();
messageBox(
400, 200,
translate("Are you sure you want to resign?"),
translate("Confirmation"),
[translate("No"), translate("Yes")],
[resumeGame, resignGame]
);
}
function exitMenuButton()
{
closeOpenDialogs();
pauseGame();
let messageTypes = {
"host": {
"caption": translate("Are you sure you want to quit? Leaving will disconnect all other players."),
"buttons": [resumeGame, leaveGame]
},
"client": {
"caption": translate("Are you sure you want to quit?"),
"buttons": [resumeGame, resignQuestion]
},
"singleplayer": {
"caption": translate("Are you sure you want to quit?"),
"buttons": [resumeGame, leaveGame]
}
};
let messageType = g_IsNetworked && g_IsController ? "host" :
(g_IsNetworked && !g_IsObserver ? "client" : "singleplayer");
messageBox(
400, 200,
messageTypes[messageType].caption,
translate("Confirmation"),
[translate("No"), translate("Yes")],
messageTypes[messageType].buttons
);
}
function resignQuestion()
{
messageBox(
400, 200,
translate("Do you want to resign or will you return soon?"),
translate("Confirmation"),
[translate("I will return"), translate("I resign")],
[leaveGame, resignGame],
[true, false]
);
}
function openDeleteDialog(selection)
{
closeOpenDialogs();
let deleteSelectedEntities = function(selectionArg)
{
Engine.PostNetworkCommand({
"type": "delete-entities",
"entities": selectionArg
});
};
messageBox(
400, 200,
translate("Destroy everything currently selected?"),
translate("Delete"),
[translate("No"), translate("Yes")],
[resumeGame, deleteSelectedEntities],
[null, selection]
);
}
function openSave()
{
closeOpenDialogs();
pauseGame();
- Engine.PushGuiPage("page_savegame.xml", {
- "savedGameData": getSavedGameData(),
- "callback": "resumeGame"
- });
+ Engine.PushGuiPage(
+ "page_savegame.xml",
+ { "savedGameData": getSavedGameData() },
+ resumeGame);
}
function openOptions()
{
closeOpenDialogs();
pauseGame();
- Engine.PushGuiPage("page_options.xml", {
- "callback": "optionsPageClosed"
- });
-}
-
-function optionsPageClosed(data)
-{
- for (let callback of data)
- if (global[callback])
- global[callback]();
+ Engine.PushGuiPage(
+ "page_options.xml",
+ {},
+ callbackFunctionNames => {
+ for (let functionName of callbackFunctionNames)
+ if (global[functionName])
+ global[functionName]();
- resumeGame();
+ resumeGame();
+ });
}
function openChat(command = "")
{
if (g_Disconnected)
return;
closeOpenDialogs();
let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
chatAddressee.selected = chatAddressee.list_data.indexOf(command);
Engine.GetGUIObjectByName("chatInput").focus();
Engine.GetGUIObjectByName("chatDialogPanel").hidden = false;
updateChatHistory();
}
function closeChat()
{
Engine.GetGUIObjectByName("chatInput").caption = "";
Engine.GetGUIObjectByName("chatInput").blur(); // Remove focus
Engine.GetGUIObjectByName("chatDialogPanel").hidden = true;
}
function resizeDiplomacyDialog()
{
let dialog = Engine.GetGUIObjectByName("diplomacyDialogPanel");
let size = dialog.size;
let tribSize = Engine.GetGUIObjectByName("diplomacyPlayer[0]_tribute[0]").size;
let widthOffset = g_ResourceData.GetCodes().length * (tribSize.right - tribSize.left) / 2;
size.left -= widthOffset;
size.right += widthOffset;
let firstRow = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size;
let heightOffset = (g_Players.length - 1) * (firstRow.bottom - firstRow.top) / 2;
size.top -= heightOffset;
size.bottom += heightOffset;
dialog.size = size;
}
function initChatWindow()
{
let filters = prepareForDropdown(g_ChatHistoryFilters);
let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
chatHistoryFilter.list = filters.text;
chatHistoryFilter.list_data = filters.key;
chatHistoryFilter.selected = 0;
Engine.GetGUIObjectByName("extendedChat").checked =
Engine.ConfigDB_GetValue("user", "chat.session.extended") == "true";
resizeChatWindow();
}
function resizeChatWindow()
{
// Hide/show the panel
let chatHistoryPage = Engine.GetGUIObjectByName("chatHistoryPage");
let extended = Engine.GetGUIObjectByName("extendedChat").checked;
chatHistoryPage.hidden = !extended;
// Resize the window
let chatDialogPanel = Engine.GetGUIObjectByName("chatDialogPanel");
if (extended)
{
chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelLarge").size;
// Adjust the width so that the chat history is in the golden ratio
let chatHistory = Engine.GetGUIObjectByName("chatHistory");
let height = chatHistory.getComputedSize().bottom - chatHistory.getComputedSize().top;
let width = (1 + Math.sqrt(5)) / 2 * height;
let size = chatDialogPanel.size;
size.left = -width / 2 - chatHistory.size.left;
size.right = width / 2 + chatHistory.size.left;
chatDialogPanel.size = size;
}
else
chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelSmall").size;
}
function updateChatHistory()
{
if (Engine.GetGUIObjectByName("chatDialogPanel").hidden ||
!Engine.GetGUIObjectByName("extendedChat").checked)
return;
let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
let selected = chatHistoryFilter.list_data[chatHistoryFilter.selected];
Engine.GetGUIObjectByName("chatHistory").caption =
g_ChatHistory.filter(msg => msg.filter[selected]).map(msg =>
Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true" ?
sprintf(translate("%(time)s %(message)s"), {
"time": msg.timePrefix,
"message": msg.txt
}) :
msg.txt
).join("\n");
}
function onToggleChatWindowExtended()
{
Engine.ConfigDB_CreateAndWriteValueToFile("user", "chat.session.extended", String(Engine.GetGUIObjectByName("extendedChat").checked), "config/user.cfg");
resizeChatWindow();
Engine.GetGUIObjectByName("chatInput").focus();
}
function openDiplomacy()
{
closeOpenDialogs();
if (g_ViewedPlayer < 1)
return;
g_IsDiplomacyOpen = true;
updateDiplomacy(true);
Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = false;
}
function closeDiplomacy()
{
g_IsDiplomacyOpen = false;
Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = true;
}
function toggleDiplomacy()
{
let open = g_IsDiplomacyOpen;
closeOpenDialogs();
if (!open)
openDiplomacy();
}
function updateDiplomacy(opening = false)
{
if (g_ViewedPlayer < 1 || !g_IsDiplomacyOpen)
return;
let simState = GetSimState();
let isCeasefireActive = simState.ceasefireActive;
let hasSharedLos = GetSimState().players[g_ViewedPlayer].hasSharedLos;
// Get offset for one line
let onesize = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size;
let rowsize = onesize.bottom - onesize.top;
// We don't include gaia
for (let i = 1; i < g_Players.length; ++i)
{
let myself = i == g_ViewedPlayer;
let playerInactive = isPlayerObserver(g_ViewedPlayer) || isPlayerObserver(i);
let hasAllies = g_Players.filter(player => player.isMutualAlly[g_ViewedPlayer]).length > 1;
diplomacySetupTexts(i, rowsize);
diplomacyFormatStanceButtons(i, myself || playerInactive || isCeasefireActive || g_Players[g_ViewedPlayer].teamsLocked);
// Tribute buttons do not need to be updated onTick, and should not because of massTributing
if (opening)
diplomacyFormatTributeButtons(i, myself || playerInactive);
diplomacyFormatAttackRequestButton(i, myself || playerInactive || isCeasefireActive || !hasAllies || !g_Players[i].isEnemy[g_ViewedPlayer]);
diplomacyFormatSpyRequestButton(i, myself || playerInactive || g_Players[i].isMutualAlly[g_ViewedPlayer] && hasSharedLos);
}
let diplomacyCeasefireCounter = Engine.GetGUIObjectByName("diplomacyCeasefireCounter");
diplomacyCeasefireCounter.caption = sprintf(
translateWithContext("ceasefire", "Remaining ceasefire time: %(time)s."),
{ "time": timeToString(simState.ceasefireTimeRemaining) }
);
diplomacyCeasefireCounter.hidden = !isCeasefireActive;
}
function diplomacySetupTexts(i, rowsize)
{
// Apply offset
let row = Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]");
let size = row.size;
size.top = rowsize * (i - 1);
size.bottom = rowsize * i;
row.size = size;
row.hidden = false;
row.sprite = "color:" + rgbToGuiColor(g_DisplayedPlayerColors[i], 32);
setOutcomeIcon(g_Players[i].state, "diplomacyPlayerOutcome[" + (i - 1) + "]");
let diplomacyPlayerName = Engine.GetGUIObjectByName("diplomacyPlayerName[" + (i - 1) + "]");
diplomacyPlayerName.caption = colorizePlayernameByID(i);
diplomacyPlayerName.tooltip = translateAISettings(g_GameAttributes.settings.PlayerData[i]);
Engine.GetGUIObjectByName("diplomacyPlayerCiv[" + (i - 1) + "]").caption = g_CivData[g_Players[i].civ].Name;
Engine.GetGUIObjectByName("diplomacyPlayerTeam[" + (i - 1) + "]").caption =
g_Players[i].team < 0 ? translateWithContext("team", "None") : g_Players[i].team + 1;
Engine.GetGUIObjectByName("diplomacyPlayerTheirs[" + (i - 1) + "]").caption =
i == g_ViewedPlayer ? "" :
g_Players[i].isAlly[g_ViewedPlayer] ?
translate("Ally") :
g_Players[i].isNeutral[g_ViewedPlayer] ? translate("Neutral") : translate("Enemy");
}
function diplomacyFormatStanceButtons(i, hidden)
{
for (let stance of ["Ally", "Neutral", "Enemy"])
{
let button = Engine.GetGUIObjectByName("diplomacyPlayer" + stance + "[" + (i - 1) + "]");
button.hidden = hidden;
if (hidden)
continue;
let isCurrentStance = g_Players[g_ViewedPlayer]["is" + stance][i];
button.caption = isCurrentStance ? translate("x") : "";
button.enabled = controlsPlayer(g_ViewedPlayer) && !isCurrentStance;
button.onPress = (function(player, stance) { return function() {
Engine.PostNetworkCommand({
"type": "diplomacy",
"player": i,
"to": stance.toLowerCase()
});
}; })(i, stance);
}
}
function diplomacyFormatTributeButtons(i, hidden)
{
let resCodes = g_ResourceData.GetCodes();
let r = 0;
for (let resCode of resCodes)
{
let button = Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]_tribute[" + r + "]");
if (!button)
{
warn("Current GUI limits prevent displaying more than " + r + " tribute buttons!");
break;
}
Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]_tribute[" + r + "]_image").sprite = "stretched:session/icons/resources/" + resCode + ".png";
button.hidden = hidden;
setPanelObjectPosition(button, r, r + 1, 0);
++r;
if (hidden)
continue;
button.enabled = controlsPlayer(g_ViewedPlayer);
button.tooltip = formatTributeTooltip(i, resCode, 100);
button.onPress = (function(i, resCode, button) {
// Shift+click to send 500, shift+click+click to send 1000, etc.
// See INPUT_MASSTRIBUTING in input.js
let multiplier = 1;
return function() {
let isBatchTrainPressed = Engine.HotkeyIsPressed("session.masstribute");
if (isBatchTrainPressed)
{
inputState = INPUT_MASSTRIBUTING;
multiplier += multiplier == 1 ? 4 : 5;
}
let amounts = {};
for (let res of resCodes)
amounts[res] = 0;
amounts[resCode] = 100 * multiplier;
button.tooltip = formatTributeTooltip(i, resCode, amounts[resCode]);
// This is in a closure so that we have access to `player`, `amounts`, and `multiplier` without some
// evil global variable hackery.
g_FlushTributing = function() {
Engine.PostNetworkCommand({ "type": "tribute", "player": i, "amounts": amounts });
multiplier = 1;
button.tooltip = formatTributeTooltip(i, resCode, 100);
};
if (!isBatchTrainPressed)
g_FlushTributing();
};
})(i, resCode, button);
}
}
function diplomacyFormatAttackRequestButton(i, hidden)
{
let button = Engine.GetGUIObjectByName("diplomacyAttackRequest[" + (i - 1) + "]");
button.hidden = hidden;
if (hidden)
return;
button.enabled = controlsPlayer(g_ViewedPlayer);
button.tooltip = translate("Request your allies to attack this enemy");
button.onPress = (function(i) { return function() {
Engine.PostNetworkCommand({ "type": "attack-request", "source": g_ViewedPlayer, "player": i });
}; })(i);
}
function diplomacyFormatSpyRequestButton(i, hidden)
{
let button = Engine.GetGUIObjectByName("diplomacySpyRequest[" + (i - 1) + "]");
let template = GetTemplateData("special/spy");
button.hidden = hidden || !template || !!GetSimState().players[g_ViewedPlayer].disabledTemplates["special/spy"];
if (button.hidden)
return;
button.enabled = controlsPlayer(g_ViewedPlayer) &&
!(g_BribeButtonsWaiting[g_ViewedPlayer] && g_BribeButtonsWaiting[g_ViewedPlayer].indexOf(i) != -1);
let modifier = "";
let tooltips = [translate("Bribe a random unit from this player and share its vision during a limited period.")];
if (!button.enabled)
modifier = "color:0 0 0 127:grayscale:";
else
{
if (template.requiredTechnology)
{
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": g_ViewedPlayer
});
if (!technologyEnabled)
{
modifier = "color:0 0 0 127:grayscale:";
button.enabled = false;
tooltips.push(getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[g_ViewedPlayer].civ));
}
}
if (template.cost)
{
let modifiedTemplate = clone(template);
for (let res in template.cost)
modifiedTemplate.cost[res] = Math.floor(GetSimState().players[i].spyCostMultiplier * template.cost[res]);
tooltips.push(getEntityCostTooltip(modifiedTemplate));
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": modifiedTemplate.cost,
"player": g_ViewedPlayer
});
let costRatio = Engine.GetTemplate("special/spy").VisionSharing.FailureCostRatio;
if (costRatio > 0)
{
tooltips.push(translate("A failed bribe will cost you:"));
for (let res in modifiedTemplate.cost)
modifiedTemplate.cost[res] = Math.floor(costRatio * modifiedTemplate.cost[res]);
tooltips.push(getEntityCostTooltip(modifiedTemplate));
}
if (neededResources)
{
if (button.enabled)
modifier = resourcesToAlphaMask(neededResources) + ":";
button.enabled = false;
tooltips.push(getNeededResourcesTooltip(neededResources));
}
}
}
let icon = Engine.GetGUIObjectByName("diplomacySpyRequestImage[" + (i - 1) + "]");
icon.sprite = modifier + "stretched:session/icons/bribes.png";
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = (function(i, button) { return function() {
Engine.PostNetworkCommand({ "type": "spy-request", "source": g_ViewedPlayer, "player": i });
if (!g_BribeButtonsWaiting[g_ViewedPlayer])
g_BribeButtonsWaiting[g_ViewedPlayer] = [];
// Don't push i twice
if (g_BribeButtonsWaiting[g_ViewedPlayer].indexOf(i) == -1)
g_BribeButtonsWaiting[g_ViewedPlayer].push(i);
diplomacyFormatSpyRequestButton(i, false);
}; })(i, button);
}
function resizeTradeDialog()
{
let dialog = Engine.GetGUIObjectByName("tradeDialogPanel");
let size = dialog.size;
let width = size.right - size.left;
let tradeSize = Engine.GetGUIObjectByName("tradeResource[0]").size;
width += g_ResourceData.GetCodes().length * (tradeSize.right - tradeSize.left);
size.left = -width / 2;
size.right = width / 2;
dialog.size = size;
}
function openTrade()
{
closeOpenDialogs();
if (g_ViewedPlayer < 1)
return;
g_IsTradeOpen = true;
let proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer);
let button = {};
let resCodes = g_ResourceData.GetCodes();
let currTradeSelection = resCodes[0];
let updateTradeButtons = function()
{
for (let res in button)
{
button[res].label.caption = proba[res] + "%";
button[res].sel.hidden = !controlsPlayer(g_ViewedPlayer) || res != currTradeSelection;
button[res].up.hidden = !controlsPlayer(g_ViewedPlayer) || res == currTradeSelection || proba[res] == 100 || proba[currTradeSelection] == 0;
button[res].dn.hidden = !controlsPlayer(g_ViewedPlayer) || res == currTradeSelection || proba[res] == 0 || proba[currTradeSelection] == 100;
}
};
hideRemaining("tradeResources", resCodes.length);
Engine.GetGUIObjectByName("tradeHelp").hidden = false;
for (let i = 0; i < resCodes.length; ++i)
{
let resCode = resCodes[i];
let barterResource = Engine.GetGUIObjectByName("barterResource[" + i + "]");
if (!barterResource)
{
warn("Current GUI limits prevent displaying more than " + i + " resources in the barter dialog!");
break;
}
// Barter:
barterOpenCommon(resCode, i, "barter");
setPanelObjectPosition(barterResource, i, i + 1);
// Trade:
let tradeResource = Engine.GetGUIObjectByName("tradeResource[" + i + "]");
if (!tradeResource)
{
warn("Current GUI limits prevent displaying more than " + i + " resources in the trading goods selection dialog!");
break;
}
setPanelObjectPosition(tradeResource, i, i + 1);
let icon = Engine.GetGUIObjectByName("tradeResourceIcon[" + i + "]");
icon.sprite = "stretched:session/icons/resources/" + resCode + ".png";
let buttonUp = Engine.GetGUIObjectByName("tradeArrowUp[" + i + "]");
let buttonDn = Engine.GetGUIObjectByName("tradeArrowDn[" + i + "]");
button[resCode] = {
"up": buttonUp,
"dn": buttonDn,
"label": Engine.GetGUIObjectByName("tradeResourceText[" + i + "]"),
"sel": Engine.GetGUIObjectByName("tradeResourceSelection[" + i + "]")
};
proba[resCode] = proba[resCode] || 0;
let buttonResource = Engine.GetGUIObjectByName("tradeResourceButton[" + i + "]");
buttonResource.enabled = controlsPlayer(g_ViewedPlayer);
buttonResource.onPress = (resource => {
return () => {
if (Engine.HotkeyIsPressed("session.fulltradeswap"))
{
for (let res of resCodes)
proba[res] = 0;
proba[resource] = 100;
Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba });
}
currTradeSelection = resource;
updateTradeButtons();
};
})(resCode);
buttonUp.enabled = controlsPlayer(g_ViewedPlayer);
buttonUp.onPress = (resource => {
return () => {
proba[resource] += Math.min(STEP, proba[currTradeSelection]);
proba[currTradeSelection] -= Math.min(STEP, proba[currTradeSelection]);
Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba });
updateTradeButtons();
};
})(resCode);
buttonDn.enabled = controlsPlayer(g_ViewedPlayer);
buttonDn.onPress = (resource => {
return () => {
proba[currTradeSelection] += Math.min(STEP, proba[resource]);
proba[resource] -= Math.min(STEP, proba[resource]);
Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba });
updateTradeButtons();
};
})(resCode);
}
updateTradeButtons();
updateTraderTexts();
Engine.GetGUIObjectByName("tradeDialogPanel").hidden = false;
}
function updateTraderTexts()
{
let traderNumber = Engine.GuiInterfaceCall("GetTraderNumber", g_ViewedPlayer);
Engine.GetGUIObjectByName("traderCountText").caption = getIdleLandTradersText(traderNumber) + "\n\n" + getIdleShipTradersText(traderNumber);
}
function initBarterButtons()
{
g_BarterSell = g_ResourceData.GetCodes()[0];
}
/**
* Code common to both the Barter Panel and the Trade/Barter Dialog, that
* only needs to be run when the panel or dialog is opened by the player.
*
* @param {string} resourceCode
* @param {number} idx - Element index within its set
* @param {string} prefix - Common prefix of the gui elements to be worked upon
*/
function barterOpenCommon(resourceCode, idx, prefix)
{
let barterButton = {};
for (let action of g_BarterActions)
barterButton[action] = Engine.GetGUIObjectByName(prefix + action + "Button[" + idx + "]");
let resource = resourceNameWithinSentence(resourceCode);
barterButton.Buy.tooltip = sprintf(translate("Buy %(resource)s"), { "resource": resource });
barterButton.Sell.tooltip = sprintf(translate("Sell %(resource)s"), { "resource": resource });
barterButton.Sell.onPress = function() {
g_BarterSell = resourceCode;
updateSelectionDetails();
updateBarterButtons();
};
}
/**
* Code common to both the Barter Panel and the Trade/Barter Dialog, that
* needs to be run on simulation update and when relevant hotkeys
* (i.e. massbarter) are pressed.
*
* @param {string} resourceCode
* @param {number} idx - Element index within its set
* @param {string} prefix - Common prefix of the gui elements to be worked upon
* @param {number} player
*/
function barterUpdateCommon(resourceCode, idx, prefix, player)
{
let barterButton = {};
let barterIcon = {};
let barterAmount = {};
for (let action of g_BarterActions)
{
barterButton[action] = Engine.GetGUIObjectByName(prefix + action + "Button[" + idx + "]");
barterIcon[action] = Engine.GetGUIObjectByName(prefix + action + "Icon[" + idx + "]");
barterAmount[action] = Engine.GetGUIObjectByName(prefix + action + "Amount[" + idx + "]");
}
let selectionIcon = Engine.GetGUIObjectByName(prefix + "SellSelection[" + idx + "]");
let amountToSell = g_BarterResourceSellQuantity;
if (Engine.HotkeyIsPressed("session.massbarter"))
amountToSell *= g_BarterMultiplier;
let isSelected = resourceCode == g_BarterSell;
let grayscale = isSelected ? "color:0 0 0 100:grayscale:" : "";
// Select color of the sell button
let neededRes = {};
neededRes[resourceCode] = amountToSell;
let canSellCurrent = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": player
}) ? "color:255 0 0 80:" : "";
// Select color of the buy button
neededRes = {};
neededRes[g_BarterSell] = amountToSell;
let canBuyAny = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": player
}) ? "color:255 0 0 80:" : "";
barterIcon.Sell.sprite = canSellCurrent + "stretched:" + grayscale + "session/icons/resources/" + resourceCode + ".png";
barterIcon.Buy.sprite = canBuyAny + "stretched:" + grayscale + "session/icons/resources/" + resourceCode + ".png";
barterAmount.Sell.caption = "-" + amountToSell;
let prices = GetSimState().players[player].barterPrices;
barterAmount.Buy.caption = "+" + Math.round(prices.sell[g_BarterSell] / prices.buy[resourceCode] * amountToSell);
barterButton.Buy.onPress = function() {
Engine.PostNetworkCommand({
"type": "barter",
"sell": g_BarterSell,
"buy": resourceCode,
"amount": amountToSell
});
};
barterButton.Buy.hidden = isSelected;
barterButton.Buy.enabled = controlsPlayer(player);
barterButton.Sell.hidden = false;
selectionIcon.hidden = !isSelected;
}
function updateBarterButtons()
{
let playerState = GetSimState().players[g_ViewedPlayer];
if (!playerState)
return;
let canBarter = playerState.canBarter;
Engine.GetGUIObjectByName("barterNoMarketsMessage").hidden = canBarter;
Engine.GetGUIObjectByName("barterResources").hidden = !canBarter;
Engine.GetGUIObjectByName("barterHelp").hidden = !canBarter;
if (canBarter)
g_ResourceData.GetCodes().forEach((resCode, i) => { barterUpdateCommon(resCode, i, "barter", g_ViewedPlayer); });
}
function getIdleLandTradersText(traderNumber)
{
let active = traderNumber.landTrader.trading;
let garrisoned = traderNumber.landTrader.garrisoned;
let inactive = traderNumber.landTrader.total - active - garrisoned;
let messageTypes = {
"active": {
"garrisoned": {
"no-inactive": translate("%(openingTradingString)s, and %(garrisonedString)s."),
"inactive": translate("%(openingTradingString)s, %(garrisonedString)s, and %(inactiveString)s.")
},
"no-garrisoned": {
"no-inactive": translate("%(openingTradingString)s."),
"inactive": translate("%(openingTradingString)s, and %(inactiveString)s.")
}
},
"no-active": {
"garrisoned": {
"no-inactive": translate("%(openingGarrisonedString)s."),
"inactive": translate("%(openingGarrisonedString)s, and %(inactiveString)s.")
},
"no-garrisoned": {
"inactive": translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive),
"no-inactive": translate("There are no land traders.")
}
}
};
let message = messageTypes[active ? "active" : "no-active"][garrisoned ? "garrisoned" : "no-garrisoned"][inactive ? "inactive" : "no-inactive"];
let activeString = sprintf(
translatePlural(
"There is %(numberTrading)s land trader trading",
"There are %(numberTrading)s land traders trading",
active
),
{ "numberTrading": active }
);
let inactiveString = sprintf(
active || garrisoned ?
translatePlural(
"%(numberOfLandTraders)s inactive",
"%(numberOfLandTraders)s inactive",
inactive
) :
translatePlural(
"%(numberOfLandTraders)s land trader inactive",
"%(numberOfLandTraders)s land traders inactive",
inactive
),
{ "numberOfLandTraders": inactive }
);
let garrisonedString = sprintf(
active || inactive ?
translatePlural(
"%(numberGarrisoned)s garrisoned on a trading merchant ship",
"%(numberGarrisoned)s garrisoned on a trading merchant ship",
garrisoned
) :
translatePlural(
"There is %(numberGarrisoned)s land trader garrisoned on a trading merchant ship",
"There are %(numberGarrisoned)s land traders garrisoned on a trading merchant ship",
garrisoned
),
{ "numberGarrisoned": garrisoned }
);
return sprintf(message, {
"openingTradingString": activeString,
"openingGarrisonedString": garrisonedString,
"garrisonedString": garrisonedString,
"inactiveString": coloredText(inactiveString, g_IdleTraderTextColor)
});
}
function getIdleShipTradersText(traderNumber)
{
let active = traderNumber.shipTrader.trading;
let inactive = traderNumber.shipTrader.total - active;
let messageTypes = {
"active": {
"inactive": translate("%(openingTradingString)s, and %(inactiveString)s."),
"no-inactive": translate("%(openingTradingString)s.")
},
"no-active": {
"inactive": translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive),
"no-inactive": translate("There are no merchant ships.")
}
};
let message = messageTypes[active ? "active" : "no-active"][inactive ? "inactive" : "no-inactive"];
let activeString = sprintf(
translatePlural(
"There is %(numberTrading)s merchant ship trading",
"There are %(numberTrading)s merchant ships trading",
active
),
{ "numberTrading": active }
);
let inactiveString = sprintf(
active ?
translatePlural(
"%(numberOfShipTraders)s inactive",
"%(numberOfShipTraders)s inactive",
inactive
) :
translatePlural(
"%(numberOfShipTraders)s merchant ship inactive",
"%(numberOfShipTraders)s merchant ships inactive",
inactive
),
{ "numberOfShipTraders": inactive }
);
return sprintf(message, {
"openingTradingString": activeString,
"inactiveString": coloredText(inactiveString, g_IdleTraderTextColor)
});
}
function closeTrade()
{
g_IsTradeOpen = false;
Engine.GetGUIObjectByName("tradeDialogPanel").hidden = true;
}
function toggleTrade()
{
let open = g_IsTradeOpen;
closeOpenDialogs();
if (!open)
openTrade();
}
function toggleTutorial()
{
let tutorialPanel = Engine.GetGUIObjectByName("tutorialPanel");
tutorialPanel.hidden = !tutorialPanel.hidden ||
!Engine.GetGUIObjectByName("tutorialText").caption;
}
function updateGameSpeedControl()
{
Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
let player = g_Players[Engine.GetPlayerID()];
g_GameSpeeds = getGameSpeedChoices(!player || player.state != "active");
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.list = g_GameSpeeds.Title;
gameSpeed.list_data = g_GameSpeeds.Speed;
let simRate = Engine.GetSimRate();
let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(+simRate.toFixed(2));
if (gameSpeedIdx == -1)
warn("Unknown gamespeed:" + simRate);
gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default;
gameSpeed.onSelectionChange = function() {
changeGameSpeed(+this.list_data[this.selected]);
};
}
function toggleGameSpeed()
{
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.hidden = !gameSpeed.hidden;
}
function toggleObjectives()
{
let open = g_IsObjectivesOpen;
closeOpenDialogs();
if (!open)
openObjectives();
}
function openObjectives()
{
g_IsObjectivesOpen = true;
let player = g_Players[Engine.GetPlayerID()];
let playerState = player && player.state;
let isActive = !playerState || playerState == "active";
Engine.GetGUIObjectByName("gameDescriptionText").caption = getGameDescription();
let objectivesPlayerstate = Engine.GetGUIObjectByName("objectivesPlayerstate");
objectivesPlayerstate.hidden = isActive;
objectivesPlayerstate.caption = g_PlayerStateMessages[playerState] || "";
let gameDescription = Engine.GetGUIObjectByName("gameDescription");
let gameDescriptionSize = gameDescription.size;
gameDescriptionSize.top = Engine.GetGUIObjectByName(
isActive ? "objectivesTitle" : "objectivesPlayerstate").size.bottom;
gameDescription.size = gameDescriptionSize;
Engine.GetGUIObjectByName("objectivesPanel").hidden = false;
}
function closeObjectives()
{
g_IsObjectivesOpen = false;
Engine.GetGUIObjectByName("objectivesPanel").hidden = true;
}
/**
* Allows players to see their own summary.
* If they have shared ally vision researched, they are able to see the summary of there allies too.
*/
function openGameSummary()
{
closeOpenDialogs();
pauseGame();
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
- Engine.PushGuiPage("page_summary.xml", {
- "sim": {
- "mapSettings": g_GameAttributes.settings,
- "playerStates": extendedSimState.players.filter((state, player) =>
- g_IsObserver || player == 0 || player == g_ViewedPlayer ||
- extendedSimState.players[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer]),
- "timeElapsed": extendedSimState.timeElapsed
- },
- "gui": {
- "dialog": true,
- "isInGame": true
+ Engine.PushGuiPage(
+ "page_summary.xml",
+ {
+ "sim": {
+ "mapSettings": g_GameAttributes.settings,
+ "playerStates": extendedSimState.players.filter((state, player) =>
+ g_IsObserver || player == 0 || player == g_ViewedPlayer ||
+ extendedSimState.players[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer]),
+ "timeElapsed": extendedSimState.timeElapsed
+ },
+ "gui": {
+ "dialog": true,
+ "isInGame": true
+ },
+ "selectedData": g_SummarySelectedData
},
- "selectedData": g_SummarySelectedData,
- "callback": "resumeGameAndSaveSummarySelectedData"
- });
+ resumeGameAndSaveSummarySelectedData);
}
-function openStrucTree()
+function openStrucTree(page)
{
closeOpenDialogs();
pauseGame();
- // TODO add info about researched techs and unlocked entities
-
- Engine.PushGuiPage(g_CivInfo.page, {
- "civ": g_CivInfo.code || g_Players[g_ViewedPlayer].civ,
- "callback": "storeCivInfoPage"
- });
+ Engine.PushGuiPage(
+ page,
+ {
+ "civ": g_CivInfo.civ || g_Players[g_ViewedPlayer].civ
+ // TODO add info about researched techs and unlocked entities
+ },
+ storeCivInfoPage);
}
function storeCivInfoPage(data)
{
- g_CivInfo.code = data.civ;
- g_CivInfo.page = data.page;
- resumeGame();
+ if (data.nextPage)
+ Engine.PushGuiPage(
+ data.nextPage,
+ { "civ": data.civ },
+ storeCivInfoPage);
+ else
+ {
+ g_CivInfo = data;
+ resumeGame();
+ }
}
/**
* Pause or resume the game.
*
* @param explicit - true if the player explicitly wants to pause or resume.
* If this argument isn't set, a multiplayer game won't be paused and the pause overlay
* won't be shown in single player.
*/
function pauseGame(pause = true, explicit = false)
{
// The NetServer only supports pausing after all clients finished loading the game.
if (g_IsNetworked && (!explicit || !g_IsNetworkedActive))
return;
if (explicit)
g_Paused = pause;
Engine.SetPaused(g_Paused || pause, !!explicit);
if (g_IsNetworked)
{
setClientPauseState(Engine.GetPlayerGUID(), g_Paused);
return;
}
updatePauseOverlay();
}
function resumeGame(explicit = false)
{
pauseGame(false, explicit);
}
function resumeGameAndSaveSummarySelectedData(data)
{
g_SummarySelectedData = data.summarySelectedData;
resumeGame(data.explicitResume);
}
/**
* Called when the current player toggles a pause button.
*/
function togglePause()
{
if (!Engine.GetGUIObjectByName("pauseButton").enabled)
return;
closeOpenDialogs();
pauseGame(!g_Paused, true);
}
/**
* Called when a client pauses or resumes in a multiplayer game.
*/
function setClientPauseState(guid, paused)
{
// Update the list of pausing clients.
let index = g_PausingClients.indexOf(guid);
if (paused && index == -1)
g_PausingClients.push(guid);
else if (!paused && index != -1)
g_PausingClients.splice(index, 1);
updatePauseOverlay();
Engine.SetPaused(!!g_PausingClients.length, false);
}
/**
* Update the pause overlay.
*/
function updatePauseOverlay()
{
Engine.GetGUIObjectByName("pauseButton").caption = g_Paused ? translate("Resume") : translate("Pause");
Engine.GetGUIObjectByName("resumeMessage").hidden = !g_Paused;
Engine.GetGUIObjectByName("pausedByText").hidden = !g_IsNetworked;
Engine.GetGUIObjectByName("pausedByText").caption = sprintf(translate("Paused by %(players)s"),
{ "players": g_PausingClients.map(guid => colorizePlayernameByGUID(guid)).join(translateWithContext("Separator for a list of players", ", ")) });
Engine.GetGUIObjectByName("pauseOverlay").hidden = !(g_Paused || g_PausingClients.length);
Engine.GetGUIObjectByName("pauseOverlay").onPress = g_Paused ? togglePause : function() {};
}
function openManual()
{
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage("page_manual.xml", {
"callback": "resumeGame"
});
}
function closeOpenDialogs()
{
closeMenu();
closeChat();
closeDiplomacy();
closeTrade();
closeObjectives();
}
function formatTributeTooltip(playerID, resourceCode, amount)
{
return sprintf(translate("Tribute %(resourceAmount)s %(resourceType)s to %(playerName)s. Shift-click to tribute %(greaterAmount)s."), {
"resourceAmount": amount,
"resourceType": resourceNameWithinSentence(resourceCode),
"playerName": colorizePlayernameByID(playerID),
"greaterAmount": amount < 500 ? 500 : amount + 500
});
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 22676)
@@ -1,1180 +1,1182 @@
/**
* Contains the layout and button settings per selection panel
*
* getItems returns a list of basic items used to fill the panel.
* This method is obligated. If the items list is empty, the panel
* won't be rendered.
*
* Then there's a loop over all items provided. In the loop,
* the item and some other standard data is added to a data object.
*
* The standard data is
* {
* "i": index
* "item": item coming from the getItems function
* "playerState": playerState
* "unitEntStates": states of the selected entities
* "rowLength": rowLength
* "numberOfItems": number of items that will be processed
* "button": gui Button object
* "icon": gui Icon object
* "guiSelection": gui button Selection overlay
* "countDisplay": gui caption space
* }
*
* Then for every data object, the setupButton function is called which
* sets the view and handlers of the button.
*/
// Cache some formation info
// Available formations per player
let g_AvailableFormations = new Map();
let g_FormationsInfo = new Map();
let g_SelectionPanels = {};
g_SelectionPanels.Alert = {
"getMaxNumberOfItems": function()
{
return 2;
},
"getItems": function(unitEntStates)
{
return unitEntStates.some(state => !!state.alertRaiser) ? ["raise", "end"] : [];
},
"setupButton": function(data)
{
data.button.onPress = function() {
switch (data.item)
{
case "raise":
raiseAlert();
return;
case "end":
endOfAlert();
return;
}
};
switch (data.item)
{
case "raise":
data.icon.sprite = "stretched:session/icons/bell_level1.png";
data.button.tooltip = translate("Raise an alert!");
break;
case "end":
data.button.tooltip = translate("End of alert.");
data.icon.sprite = "stretched:session/icons/bell_level0.png";
break;
}
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, this.getMaxNumberOfItems() - data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Barter = {
"getMaxNumberOfItems": function()
{
return 4;
},
"rowLength": 4,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
// If more than `rowLength` resources, don't display icons.
if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetCodes().length > this.rowLength)
return [];
return g_ResourceData.GetCodes();
},
"setupButton": function(data)
{
barterOpenCommon(data.item, data.i, "unitBarter");
barterUpdateCommon(data.item, data.i, "unitBarter", data.player);
let button = {};
for (let action of g_BarterActions)
button[action] = Engine.GetGUIObjectByName("unitBarter" + action + "Button[" + data.i + "]");
setPanelObjectPosition(button.Sell, data.i, data.rowLength);
setPanelObjectPosition(button.Buy, data.i + data.rowLength, data.rowLength);
return true;
}
};
g_SelectionPanels.Command = {
"getMaxNumberOfItems": function()
{
return 6;
},
"getItems": function(unitEntStates)
{
let commands = [];
for (let command in g_EntityCommands)
{
let info = g_EntityCommands[command].getInfo(unitEntStates);
if (info)
{
info.name = command;
commands.push(info);
}
}
return commands;
},
"setupButton": function(data)
{
data.button.tooltip = data.item.tooltip;
data.button.onPress = function() {
if (data.item.callback)
data.item.callback(data.item);
else
performCommand(data.unitEntStates, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled =
g_IsObserver && data.item.name == "focus-rally" ||
controlsPlayer(data.player) && (data.item.name != "delete" ||
data.unitEntStates.some(state => !isUndeletable(state)));
data.icon.sprite = "stretched:session/icons/" + data.item.icon;
let size = data.button.size;
// relative to the center ( = 50%)
size.rleft = 50;
size.rright = 50;
// offset from the center calculation, count on square buttons, so size.bottom is the width too
size.left = (data.i - data.numberOfItems / 2) * (size.bottom + 1);
size.right = size.left + size.bottom;
data.button.size = size;
return true;
}
};
g_SelectionPanels.AllyCommand = {
"getMaxNumberOfItems": function()
{
return 2;
},
"conflictsWith": ["Command"],
"getItems": function(unitEntStates)
{
let commands = [];
for (let command in g_AllyEntityCommands)
for (let state of unitEntStates)
{
let info = g_AllyEntityCommands[command].getInfo(state);
if (info)
{
info.name = command;
commands.push(info);
break;
}
}
return commands;
},
"setupButton": function(data)
{
data.button.tooltip = data.item.tooltip;
data.button.onPress = function() {
if (data.item.callback)
data.item.callback(data.item);
else
performAllyCommand(data.unitEntStates[0].id, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled = !!data.item.count;
let grayscale = data.button.enabled ? "" : "grayscale:";
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + data.item.icon;
let size = data.button.size;
// relative to the center ( = 50%)
size.rleft = 50;
size.rright = 50;
// offset from the center calculation, count on square buttons, so size.bottom is the width too
size.left = (data.i - data.numberOfItems / 2) * (size.bottom + 1);
size.right = size.left + size.bottom;
data.button.size = size;
return true;
}
};
g_SelectionPanels.Construction = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function()
{
return getAllBuildableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item);
if (!template)
return false;
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.player
});
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, 1),
"player": data.player
});
data.button.onPress = function() { startBuildingPlacement(data.item, data.playerState); };
data.button.onPressRight = function() { showTemplateDetails(data.item); };
let tooltips = [
getEntityNamesFormatted,
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip,
getEntityCostTooltip,
getGarrisonTooltip,
getPopulationBonusTooltip,
showTemplateViewerOnRightClickTooltip
].map(func => func(template));
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
data.button.enabled = controlsPlayer(data.player);
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Formation = {
"getMaxNumberOfItems": function()
{
return 16;
},
"rowLength": 4,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !hasClass(state, "Unit")))
return [];
if (!g_AvailableFormations.has(unitEntStates[0].player))
g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player));
let availableFormations = g_AvailableFormations.get(unitEntStates[0].player);
// Hide the panel if all formations are disabled
if (availableFormations.some(formation => canMoveSelectionIntoFormation(formation)))
return availableFormations;
return [];
},
"setupButton": function(data)
{
if (!g_FormationsInfo.has(data.item))
g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item }));
let formationInfo = g_FormationsInfo.get(data.item);
let formationOk = canMoveSelectionIntoFormation(data.item);
let unitIds = data.unitEntStates.map(state => state.id);
let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": unitIds,
"formationTemplate": data.item
});
data.button.onPress = function() {
performFormation(unitIds, data.item);
};
let tooltip = translate(formationInfo.name);
if (!formationOk && formationInfo.tooltip)
tooltip += "\n" + coloredText(translate(formationInfo.tooltip), "red");
data.button.tooltip = tooltip;
data.button.enabled = formationOk && controlsPlayer(data.player);
let grayscale = formationOk ? "" : "grayscale:";
data.guiSelection.hidden = !formationSelected;
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Garrison = {
"getMaxNumberOfItems": function()
{
return 12;
},
"rowLength": 4,
"conflictsWith": ["Barter"],
"getItems": function(unitEntStates)
{
if (unitEntStates.every(state => !state.garrisonHolder))
return [];
let groups = new EntityGroups();
for (let state of unitEntStates)
if (state.garrisonHolder)
groups.add(state.garrisonHolder.entities);
return groups.getEntsGrouped();
},
"setupButton": function(data)
{
let entState = GetEntityState(data.item.ents[0]);
let template = GetTemplateData(entState.template);
if (!template)
return false;
data.button.onPress = function() {
unloadTemplate(template.selectionGroupName || entState.template, entState.player);
};
data.countDisplay.caption = data.item.ents.length || "";
let canUngarrison =
g_ViewedPlayer == data.player ||
g_ViewedPlayer == entState.player;
data.button.enabled = canUngarrison && controlsPlayer(g_ViewedPlayer);
data.button.tooltip = (canUngarrison || g_IsObserver ?
sprintf(translate("Unload %(name)s"), { "name": getEntityNames(template) }) + "\n" +
translate("Single-click to unload 1. Shift-click to unload all of this type.") :
getEntityNames(template)) + "\n" +
sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[entState.player].name
});
data.guiSelection.sprite = getPlayerHighlightColor(entState.player);
data.button.sprite_disabled = data.button.sprite;
// Selection panel buttons only appear disabled if they
// also appear disabled to the owner of the building.
data.icon.sprite =
(canUngarrison || g_IsObserver ? "" : "grayscale:") +
"stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Gate = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntStates)
{
let hideLocked = unitEntStates.every(state => !state.gate || !state.gate.locked);
let hideUnlocked = unitEntStates.every(state => !state.gate || state.gate.locked);
if (hideLocked && hideUnlocked)
return [];
return [
{
"hidden": hideLocked,
"tooltip": translate("Lock Gate"),
"icon": "session/icons/lock_locked.png",
"locked": true
},
{
"hidden": hideUnlocked,
"tooltip": translate("Unlock Gate"),
"icon": "session/icons/lock_unlocked.png",
"locked": false
}
];
},
"setupButton": function(data)
{
data.button.onPress = function() { lockGate(data.item.locked); };
data.button.tooltip = data.item.tooltip;
data.button.enabled = controlsPlayer(data.player);
data.guiSelection.hidden = data.item.hidden;
data.icon.sprite = "stretched:" + data.item.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Pack = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntStates)
{
let checks = {};
for (let state of unitEntStates)
{
if (!state.pack)
continue;
if (state.pack.progress == 0)
{
if (state.pack.packed)
checks.unpackButton = true;
else
checks.packButton = true;
}
else if (state.pack.packed)
checks.unpackCancelButton = true;
else
checks.packCancelButton = true;
}
let items = [];
if (checks.packButton)
items.push({
"packing": false,
"packed": false,
"tooltip": translate("Pack"),
"callback": function() { packUnit(true); }
});
if (checks.unpackButton)
items.push({
"packing": false,
"packed": true,
"tooltip": translate("Unpack"),
"callback": function() { packUnit(false); }
});
if (checks.packCancelButton)
items.push({
"packing": true,
"packed": false,
"tooltip": translate("Cancel Packing"),
"callback": function() { cancelPackUnit(true); }
});
if (checks.unpackCancelButton)
items.push({
"packing": true,
"packed": true,
"tooltip": translate("Cancel Unpacking"),
"callback": function() { cancelPackUnit(false); }
});
return items;
},
"setupButton": function(data)
{
data.button.onPress = function() {data.item.callback(data.item); };
data.button.tooltip = data.item.tooltip;
if (data.item.packing)
data.icon.sprite = "stretched:session/icons/cancel.png";
else if (data.item.packed)
data.icon.sprite = "stretched:session/icons/unpack.png";
else
data.icon.sprite = "stretched:session/icons/pack.png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Queue = {
"getMaxNumberOfItems": function()
{
return 16;
},
/**
* Returns a list of all items in the productionqueue of the selection
* The first entry of every entity's production queue will come before
* the second entry of every entity's production queue
*/
"getItems": function(unitEntStates)
{
let queue = [];
let foundNew = true;
for (let i = 0; foundNew; ++i)
{
foundNew = false;
for (let state of unitEntStates)
{
if (!state.production || !state.production.queue[i])
continue;
queue.push({
"producingEnt": state.id,
"queuedItem": state.production.queue[i]
});
foundNew = true;
}
}
return queue;
},
"resizePanel": function(numberOfItems, rowLength)
{
let numRows = Math.ceil(numberOfItems / rowLength);
let panel = Engine.GetGUIObjectByName("unitQueuePanel");
let size = panel.size;
let buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom;
let margin = 4;
size.top = size.bottom - numRows * buttonSize - (numRows + 2) * margin;
panel.size = size;
},
"setupButton": function(data)
{
let queuedItem = data.item.queuedItem;
// Differentiate between units and techs
let template;
if (queuedItem.unitTemplate)
template = GetTemplateData(queuedItem.unitTemplate);
else if (queuedItem.technologyTemplate)
template = GetTechnologyData(queuedItem.technologyTemplate, GetSimState().players[data.player].civ);
else
{
warning("Unknown production queue template " + uneval(queuedItem));
return false;
}
data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, queuedItem.id); };
let tooltip = getEntityNames(template);
if (queuedItem.neededSlots)
{
tooltip += "\n" + coloredText(translate("Insufficient population capacity:"), "red");
tooltip += "\n" + sprintf(translate("%(population)s %(neededSlots)s"), {
"population": resourceIcon("population"),
"neededSlots": queuedItem.neededSlots
});
}
data.button.tooltip = tooltip;
data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : "";
// Show the time remaining to finish the first item
if (data.i == 0)
Engine.GetGUIObjectByName("queueTimeRemaining").caption =
Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss"));
let guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]");
let size = guiObject.size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(queuedItem.progress * (size.right - size.left));
guiObject.size = size;
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Research = {
"getMaxNumberOfItems": function()
{
return 8;
},
"getItems": function(unitEntStates)
{
let ret = [];
if (unitEntStates.length == 1)
return !unitEntStates[0].production || !unitEntStates[0].production.technologies ? ret :
unitEntStates[0].production.technologies.map(tech => ({
"tech": tech,
"techCostMultiplier": unitEntStates[0].production.techCostMultiplier,
"researchFacilityId": unitEntStates[0].id
}));
for (let state of unitEntStates)
{
if (!state.production || !state.production.technologies)
continue;
// Remove the techs we already have in ret (with the same name and techCostMultiplier)
let filteredTechs = state.production.technologies.filter(
tech => tech != null && !ret.some(
item =>
(item.tech == tech ||
item.tech.pair &&
tech.pair &&
item.tech.bottom == tech.bottom &&
item.tech.top == tech.top) &&
Object.keys(item.techCostMultiplier).every(
k => item.techCostMultiplier[k] == state.production.techCostMultiplier[k])
));
if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() &&
getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2))
ret = ret.concat(filteredTechs.map(tech => ({
"tech": tech,
"techCostMultiplier": state.production.techCostMultiplier,
"researchFacilityId": state.id
})));
}
return ret;
},
"hideItem": function(i, rowLength) // Called when no item is found
{
Engine.GetGUIObjectByName("unitResearchButton[" + i + "]").hidden = true;
// We also remove the paired tech and the pair symbol
Engine.GetGUIObjectByName("unitResearchButton[" + (i + rowLength) + "]").hidden = true;
Engine.GetGUIObjectByName("unitResearchPair[" + i + "]").hidden = true;
},
"setupButton": function(data)
{
if (!data.item.tech)
{
g_SelectionPanels.Research.hideItem(data.i, data.rowLength);
return false;
}
// Start position (start at the bottom)
let position = data.i + data.rowLength;
// Only show the top button for pairs
if (!data.item.tech.pair)
Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true;
// Set up the tech connector
let pair = Engine.GetGUIObjectByName("unitResearchPair[" + data.i + "]");
pair.hidden = data.item.tech.pair == null;
setPanelObjectPosition(pair, data.i, data.rowLength);
// Handle one or two techs (tech pair)
let player = data.player;
let playerState = GetSimState().players[player];
for (let tech of data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech])
{
// Don't change the object returned by GetTechnologyData
let template = clone(GetTechnologyData(tech, playerState.civ));
if (!template)
return false;
for (let res in template.cost)
template.cost[res] *= data.item.techCostMultiplier[res];
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": template.cost,
"player": player
});
let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
"tech": tech,
"player": player
});
let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]");
let icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]");
let tooltips = [
getEntityNamesFormatted,
getEntityTooltip,
getEntityCostTooltip,
showTemplateViewerOnRightClickTooltip
].map(func => func(template));
if (!requirementsPassed)
{
let tip = template.requirementsTooltip;
let reqs = template.reqs;
for (let req of reqs)
{
if (!req.entities)
continue;
let entityCounts = [];
for (let entity of req.entities)
{
let current = 0;
switch (entity.check)
{
case "count":
current = playerState.classCounts[entity.class] || 0;
break;
case "variants":
current = playerState.typeCountsByClass[entity.class] ?
Object.keys(playerState.typeCountsByClass[entity.class]).length : 0;
break;
}
let remaining = entity.number - current;
if (remaining < 1)
continue;
entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), {
"number": remaining,
"class": entity.class
}));
}
tip += " " + sprintf(translate("Remaining: %(entityCounts)s"), {
"entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", "))
});
}
tooltips.push(tip);
}
tooltips.push(getNeededResourcesTooltip(neededResources));
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = (t => function() {
addResearchToQueue(data.item.researchFacilityId, t);
})(tech);
button.onPressRight = (t => function () {
showTemplateDetails(
t,
GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv);
})(tech);
if (data.item.tech.pair)
{
// On mouse enter, show a cross over the other icon
let unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]");
button.onMouseEnter = function() {
unchosenIcon.hidden = false;
};
button.onMouseLeave = function() {
unchosenIcon.hidden = true;
};
}
button.hidden = false;
let modifier = "";
if (!requirementsPassed)
{
button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
button.enabled = controlsPlayer(data.player);
if (template.icon)
icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(button, position, data.rowLength);
// Prepare to handle the top button (if any)
position -= data.rowLength;
}
return true;
}
};
g_SelectionPanels.Selection = {
"getMaxNumberOfItems": function()
{
return 16;
},
"rowLength": 4,
"getItems": function(unitEntStates)
{
if (unitEntStates.length < 2)
return [];
return g_Selection.groups.getEntsGrouped();
},
"setupButton": function(data)
{
let entState = GetEntityState(data.item.ents[0]);
let template = GetTemplateData(entState.template);
if (!template)
return false;
for (let ent of data.item.ents)
{
let state = GetEntityState(ent);
if (state.resourceCarrying && state.resourceCarrying.length !== 0)
{
if (!data.carried)
data.carried = {};
let carrying = state.resourceCarrying[0];
if (data.carried[carrying.type])
data.carried[carrying.type] += carrying.amount;
else
data.carried[carrying.type] = carrying.amount;
}
if (state.trader && state.trader.goods && state.trader.goods.amount)
{
if (!data.carried)
data.carried = {};
let amount = state.trader.goods.amount;
let type = state.trader.goods.type;
let totalGain = amount.traderGain;
if (amount.market1Gain)
totalGain += amount.market1Gain;
if (amount.market2Gain)
totalGain += amount.market2Gain;
if (data.carried[type])
data.carried[type] += totalGain;
else
data.carried[type] = totalGain;
}
}
let unitOwner = GetEntityState(data.item.ents[0]).player;
let tooltip = getEntityNames(template);
if (data.carried)
tooltip += "\n" + Object.keys(data.carried).map(res =>
resourceIcon(res) + data.carried[res]
).join(" ");
if (g_IsObserver)
tooltip += "\n" + sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[unitOwner].name
});
data.button.tooltip = tooltip;
data.guiSelection.sprite = getPlayerHighlightColor(unitOwner);
data.guiSelection.hidden = !g_IsObserver;
data.countDisplay.caption = data.item.ents.length || "";
data.button.onPress = function() { changePrimarySelectionGroup(data.item.key, false); };
data.button.onPressRight = function() { changePrimarySelectionGroup(data.item.key, true); };
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Stance = {
"getMaxNumberOfItems": function()
{
return 5;
},
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal")))
return [];
return unitEntStates[0].unitAI.selectableStances;
},
"setupButton": function(data)
{
let unitIds = data.unitEntStates.map(state => state.id);
data.button.onPress = function() { performStance(unitIds, data.item); };
data.button.tooltip = getStanceDisplayName(data.item) + "\n" +
"[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]";
data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": unitIds,
"stance": data.item
});
data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Training = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function()
{
return getAllTrainableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item);
if (!template)
return false;
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.player
});
let unitIds = data.unitEntStates.map(status => status.id);
let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingStatus(unitIds, data.item, data.playerState);
let trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, trainNum),
"player": data.player
});
data.button.onPress = function() {
if (!neededResources)
addTrainingToQueue(unitIds, data.item, data.playerState);
};
data.button.onPressRight = function() {
showTemplateDetails(data.item);
};
data.countDisplay.caption = trainNum > 1 ? trainNum : "";
let tooltips = [
"[font=\"sans-bold-16\"]" +
colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) +
"[/font]" + " " + getEntityNamesFormatted(template),
getVisibleEntityClassesFormatted(template),
getAurasTooltip(template),
getEntityTooltip(template),
getEntityCostTooltip(template, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
];
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers));
if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
tooltips = tooltips.concat([
getHealthTooltip,
getAttackTooltip,
getSplashDamageTooltip,
getHealerTooltip,
getArmorTooltip,
getGarrisonTooltip,
getProjectilesTooltip,
getSpeedTooltip
].map(func => func(template)));
tooltips.push(showTemplateViewerOnRightClickTooltip());
tooltips.push(
formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch),
getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else
{
data.button.enabled = controlsPlayer(data.player);
if (neededResources)
modifier = resourcesToAlphaMask(neededResources) + ":";
}
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
let index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
return true;
}
};
g_SelectionPanels.Upgrade = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntStates)
{
// Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units.
if (unitEntStates.some(state => state.template != unitEntStates[0].template))
return false;
return unitEntStates[0].upgrade && unitEntStates[0].upgrade.upgrades;
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item.entity);
if (!template)
return false;
let technologyEnabled = true;
if (data.item.requiredTechnology)
technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": data.item.requiredTechnology,
"player": data.player
});
let neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(data.item, data.unitEntStates.length),
"player": data.player
});
let limits = getEntityLimitAndCount(data.playerState, data.item.entity);
let progress = data.unitEntStates[0].upgrade.progress || 0;
let isUpgrading = data.unitEntStates[0].upgrade.template == data.item.entity;
let tooltip;
if (!progress)
{
let tooltips = [];
if (data.item.tooltip)
tooltips.push(sprintf(translate("Upgrade into a %(name)s. %(tooltip)s"), {
"name": template.name.generic,
"tooltip": translate(data.item.tooltip)
}));
else
tooltips.push(sprintf(translate("Upgrade into a %(name)s."), {
"name": template.name.generic
}));
tooltips.push(
getEntityCostComponentsTooltipString(data.item, undefined, data.unitEntStates.length),
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
getRequiredTechnologyTooltip(technologyEnabled, data.item.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources),
showTemplateViewerOnRightClickTooltip());
tooltip = tooltips.filter(tip => tip).join("\n");
data.button.onPress = function() { upgradeEntity(data.item.entity); };
}
else if (isUpgrading)
{
tooltip = translate("Cancel Upgrading");
data.button.onPress = function() { cancelUpgradeEntity(); };
}
else
{
tooltip = translate("Cannot upgrade when the entity is already upgrading.");
data.button.onPress = function() {};
}
data.button.enabled = controlsPlayer(data.player);
data.button.tooltip = tooltip;
data.button.onPressRight = function() {
showTemplateDetails(data.item.entity);
};
let modifier = "";
if (!isUpgrading)
if (progress || !technologyEnabled || limits.canBeAddedCount == 0 &&
!hasSameRestrictionCategory(data.item.entity, data.unitEntStates[0].template))
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier = resourcesToAlphaMask(neededResources) + ":";
}
data.icon.sprite = modifier + "stretched:session/" +
(data.item.icon || "portraits/" + template.icon);
data.countDisplay.caption = data.unitEntStates.length > 1 ? data.unitEntStates.length : "";
let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]");
if (isUpgrading)
{
let size = progressOverlay.size;
size.top = size.left + Math.round(progress * (size.right - size.left));
progressOverlay.size = size;
}
progressOverlay.hidden = !isUpgrading;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
/**
* Pauses game and opens the template details viewer for a selected entity or technology.
*
* Technologies don't have a set civ, so we pass along the native civ of
* the template of the entity that's researching it.
*
* @param {string} [civCode] - The template name of the entity that researches the selected technology.
*/
function showTemplateDetails(templateName, civCode)
{
pauseGame();
- Engine.PushGuiPage("page_viewer.xml", {
- "templateName": templateName,
- "callback": "resumeGame",
- "civ": civCode
- });
+ Engine.PushGuiPage(
+ "page_viewer.xml",
+ {
+ "templateName": templateName,
+ "civ": civCode
+ },
+ resumeGame);
}
/**
* If two panels need the same space, so they collide,
* the one appearing first in the order is rendered.
*
* Note that the panel needs to appear in the list to get rendered.
*/
let g_PanelsOrder = [
// LEFT PANE
"Barter", // Must always be visible on markets
"Garrison", // More important than Formation, as you want to see the garrisoned units in ships
"Alert",
"Formation",
"Stance", // Normal together with formation
// RIGHT PANE
"Gate", // Must always be shown on gates
"Pack", // Must always be shown on packable entities
"Upgrade", // Must always be shown on upgradable entities
"Training",
"Construction",
"Research", // Normal together with training
// UNIQUE PANES (importance doesn't matter)
"Command",
"AllyCommand",
"Queue",
"Selection",
];
Index: ps/trunk/binaries/data/mods/public/gui/session/top_panel/civ_icon.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/top_panel/civ_icon.xml (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/session/top_panel/civ_icon.xml (revision 22676)
@@ -1,11 +1,11 @@
- openStrucTree()
+ openStrucTree(g_CivInfo.page)
Index: ps/trunk/binaries/data/mods/public/gui/summary/summary.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/summary/summary.js (revision 22675)
+++ ps/trunk/binaries/data/mods/public/gui/summary/summary.js (revision 22676)
@@ -1,562 +1,562 @@
const g_CivData = loadCivData(false, false);
var g_ScorePanelsData;
var g_MaxHeadingTitle = 9;
var g_LongHeadingWidth = 250;
var g_PlayerBoxYSize = 40;
var g_PlayerBoxGap = 2;
var g_PlayerBoxAlpha = 50;
var g_TeamsBoxYStart = 40;
var g_TypeColors = {
"blue": "196 198 255",
"green": "201 255 200",
"red": "255 213 213",
"yellow": "255 255 157"
};
/**
* Colors, captions and format used for units, buildings, etc. types
*/
var g_SummaryTypes = {
"percent": {
"color": "",
"caption": "%",
"postfix": "%"
},
"trained": {
"color": g_TypeColors.green,
"caption": translate("Trained"),
"postfix": " / "
},
"constructed": {
"color": g_TypeColors.green,
"caption": translate("Constructed"),
"postfix": " / "
},
"gathered": {
"color": g_TypeColors.green,
"caption": translate("Gathered"),
"postfix": " / "
},
"sent": {
"color": g_TypeColors.green,
"caption": translate("Sent"),
"postfix": " / "
},
"bought": {
"color": g_TypeColors.green,
"caption": translate("Bought"),
"postfix": " / "
},
"income": {
"color": g_TypeColors.green,
"caption": translate("Income"),
"postfix": " / "
},
"captured": {
"color": g_TypeColors.yellow,
"caption": translate("Captured"),
"postfix": " / "
},
"succeeded": {
"color": g_TypeColors.green,
"caption": translate("Succeeded"),
"postfix": " / "
},
"destroyed": {
"color": g_TypeColors.blue,
"caption": translate("Destroyed"),
"postfix": "\n"
},
"killed": {
"color": g_TypeColors.blue,
"caption": translate("Killed"),
"postfix": "\n"
},
"lost": {
"color": g_TypeColors.red,
"caption": translate("Lost"),
"postfix": ""
},
"used": {
"color": g_TypeColors.red,
"caption": translate("Used"),
"postfix": ""
},
"received": {
"color": g_TypeColors.red,
"caption": translate("Received"),
"postfix": ""
},
"sold": {
"color": g_TypeColors.red,
"caption": translate("Sold"),
"postfix": ""
},
"outcome": {
"color": g_TypeColors.red,
"caption": translate("Outcome"),
"postfix": ""
},
"failed": {
"color": g_TypeColors.red,
"caption": translate("Failed"),
"postfix": ""
}
};
// Translation: Unicode encoded infinity symbol indicating a division by zero in the summary screen.
var g_InfinitySymbol = translate("\u221E");
var g_Teams = [];
// TODO set g_PlayerCount as playerCounters.length
var g_PlayerCount = 0;
var g_GameData;
var g_ResourceData = new Resources();
/**
* Selected chart indexes.
*/
var g_SelectedChart = {
"category": [0, 0],
"value": [0, 1],
"type": [0, 0]
};
/**
* Array of the panel button names.
*/
var g_PanelButtons = [];
/**
* Remember the name of the currently opened view panel.
*/
var g_SelectedPanel = "";
function init(data)
{
// Fill globals
g_GameData = data;
g_ScorePanelsData = getScorePanelsData();
g_PanelButtons = Object.keys(g_ScorePanelsData).concat(["charts"]).map(panel => panel + "PanelButton");
g_SelectedPanel = g_PanelButtons[0];
if (data && data.selectedData)
{
g_SelectedPanel = data.selectedData.panel;
g_SelectedChart = data.selectedData.charts;
}
initTeamData();
calculateTeamCounterDataHelper();
// Output globals
initGUIWindow();
initPlayerBoxPositions();
initGUICharts();
initGUILabels();
initGUIButtons();
selectPanel(Engine.GetGUIObjectByName(g_SelectedPanel));
for (let button of g_PanelButtons)
{
let tab = Engine.GetGUIObjectByName(button);
tab.onMouseWheelUp = () => selectNextTab(1);
tab.onMouseWheelDown = () => selectNextTab(-1);
}
}
/**
* Sets the style and title of the page.
*/
function initGUIWindow()
{
let summaryWindow = Engine.GetGUIObjectByName("summaryWindow");
summaryWindow.sprite = g_GameData.gui.dialog ? "ModernDialog" : "ModernWindow";
summaryWindow.size = g_GameData.gui.dialog ? "16 24 100%-16 100%-24" : "0 0 100% 100%";
Engine.GetGUIObjectByName("summaryWindowTitle").size = g_GameData.gui.dialog ? "50%-128 -16 50%+128 16" : "50%-128 4 50%+128 36";
}
/**
* Show next/previous panel.
* @param direction - 1/-1 forward, backward panel.
*/
function selectNextTab(direction)
{
selectPanel(Engine.GetGUIObjectByName(g_PanelButtons[
(g_PanelButtons.indexOf(g_SelectedPanel) + direction + g_PanelButtons.length) % g_PanelButtons.length]));
}
function selectPanel(panel)
{
// TODO: move panel buttons to a custom parent object
for (let button of Engine.GetGUIObjectByName("summaryWindow").children)
if (button.name.endsWith("PanelButton"))
button.sprite = "ModernTabHorizontalBackground";
panel.sprite = "ModernTabHorizontalForeground";
adjustTabDividers(panel.size);
let generalPanel = Engine.GetGUIObjectByName("generalPanel");
let chartsPanel = Engine.GetGUIObjectByName("chartsPanel");
let chartsHidden = panel.name != "chartsPanelButton";
generalPanel.hidden = !chartsHidden;
chartsPanel.hidden = chartsHidden;
if (chartsHidden)
updatePanelData(g_ScorePanelsData[panel.name.substr(0, panel.name.length - "PanelButton".length)]);
else
[0, 1].forEach(updateCategoryDropdown);
g_SelectedPanel = panel.name;
}
function initGUICharts()
{
let player_colors = [];
for (let i = 1; i <= g_PlayerCount; ++i)
{
let playerState = g_GameData.sim.playerStates[i];
player_colors.push(
Math.floor(playerState.color.r * 255) + " " +
Math.floor(playerState.color.g * 255) + " " +
Math.floor(playerState.color.b * 255)
);
}
for (let i = 0; i < 2; ++i)
Engine.GetGUIObjectByName("chart[" + i + "]").series_color = player_colors;
let chartLegend = Engine.GetGUIObjectByName("chartLegend");
chartLegend.caption = g_GameData.sim.playerStates.slice(1).map(
(state, index) => coloredText("■", player_colors[index]) + " " + state.name
).join(" ");
let chart1Part = Engine.GetGUIObjectByName("chart[1]Part");
let chart1PartSize = chart1Part.size;
chart1PartSize.rright += 50;
chart1PartSize.rleft += 50;
chart1PartSize.right -= 5;
chart1PartSize.left -= 5;
chart1Part.size = chart1PartSize;
}
function resizeDropdown(dropdown)
{
let size = dropdown.size;
size.bottom = dropdown.size.top +
(Engine.GetTextWidth(dropdown.font, dropdown.list[dropdown.selected]) >
dropdown.size.right - dropdown.size.left - 32 ? 42 : 27);
dropdown.size = size;
}
function updateCategoryDropdown(number)
{
let chartCategory = Engine.GetGUIObjectByName("chart[" + number + "]CategorySelection");
chartCategory.list_data = Object.keys(g_ScorePanelsData);
chartCategory.list = Object.keys(g_ScorePanelsData).map(panel => g_ScorePanelsData[panel].caption);
chartCategory.onSelectionChange = function() {
if (!this.list_data[this.selected])
return;
if (g_SelectedChart.category[number] != this.selected)
{
g_SelectedChart.category[number] = this.selected;
g_SelectedChart.value[number] = 0;
g_SelectedChart.type[number] = 0;
}
resizeDropdown(this);
updateValueDropdown(number, this.list_data[this.selected]);
};
chartCategory.selected = g_SelectedChart.category[number];
}
function updateValueDropdown(number, category)
{
let chartValue = Engine.GetGUIObjectByName("chart[" + number + "]ValueSelection");
let list = g_ScorePanelsData[category].headings.map(heading => heading.caption);
list.shift();
chartValue.list = list;
let list_data = g_ScorePanelsData[category].headings.map(heading => heading.identifier);
list_data.shift();
chartValue.list_data = list_data;
chartValue.onSelectionChange = function() {
if (!this.list_data[this.selected])
return;
if (g_SelectedChart.value[number] != this.selected)
{
g_SelectedChart.value[number] = this.selected;
g_SelectedChart.type[number] = 0;
}
resizeDropdown(this);
updateTypeDropdown(number, category, this.list_data[this.selected], this.selected);
};
chartValue.selected = g_SelectedChart.value[number];
}
function updateTypeDropdown(number, category, item, itemNumber)
{
let testValue = g_ScorePanelsData[category].counters[itemNumber].fn(g_GameData.sim.playerStates[1], 0, item);
let hide = !g_ScorePanelsData[category].counters[itemNumber].fn ||
typeof testValue != "object" || Object.keys(testValue).length < 2;
Engine.GetGUIObjectByName("chart[" + number + "]TypeLabel").hidden = hide;
let chartType = Engine.GetGUIObjectByName("chart[" + number + "]TypeSelection");
chartType.hidden = hide;
if (hide)
{
updateChart(number, category, item, itemNumber, Object.keys(testValue)[0] || undefined);
return;
}
chartType.list = Object.keys(testValue).map(type => g_SummaryTypes[type].caption);
chartType.list_data = Object.keys(testValue);
chartType.onSelectionChange = function() {
if (!this.list_data[this.selected])
return;
g_SelectedChart.type[number] = this.selected;
resizeDropdown(this);
updateChart(number, category, item, itemNumber, this.list_data[this.selected]);
};
chartType.selected = g_SelectedChart.type[number];
}
function updateChart(number, category, item, itemNumber, type)
{
if (!g_ScorePanelsData[category].counters[itemNumber].fn)
return;
let chart = Engine.GetGUIObjectByName("chart[" + number + "]");
chart.format_y = g_ScorePanelsData[category].headings[itemNumber + 1].format || "INTEGER";
Engine.GetGUIObjectByName("chart[" + number + "]XAxisLabel").caption = translate("Time elapsed");
let series = [];
for (let j = 1; j <= g_PlayerCount; ++j)
{
let playerState = g_GameData.sim.playerStates[j];
let data = [];
for (let index in playerState.sequences.time)
{
let value = g_ScorePanelsData[category].counters[itemNumber].fn(playerState, index, item);
if (type)
value = value[type];
data.push([playerState.sequences.time[index], value]);
}
series.push(data);
}
chart.series = series;
}
function adjustTabDividers(tabSize)
{
let leftSpacer = Engine.GetGUIObjectByName("tabDividerLeft");
let rightSpacer = Engine.GetGUIObjectByName("tabDividerRight");
leftSpacer.size = [
20,
leftSpacer.size.top,
tabSize.left + 2,
leftSpacer.size.bottom
].join(" ");
rightSpacer.size = [
tabSize.right - 2,
rightSpacer.size.top,
"100%-20",
rightSpacer.size.bottom
].join(" ");
}
function updatePanelData(panelInfo)
{
resetGeneralPanel();
updateGeneralPanelHeadings(panelInfo.headings);
updateGeneralPanelTitles(panelInfo.titleHeadings);
let rowPlayerObjectWidth = updateGeneralPanelCounter(panelInfo.counters);
updateGeneralPanelTeams();
let index = g_GameData.sim.playerStates[1].sequences.time.length - 1;
let playerBoxesCounts = [];
for (let i = 0; i < g_PlayerCount; ++i)
{
let playerState = g_GameData.sim.playerStates[i + 1];
if (!playerBoxesCounts[playerState.team + 1])
playerBoxesCounts[playerState.team + 1] = 1;
else
playerBoxesCounts[playerState.team + 1] += 1;
let positionObject = playerBoxesCounts[playerState.team + 1] - 1;
let rowPlayer = "playerBox[" + positionObject + "]";
let playerOutcome = "playerOutcome[" + positionObject + "]";
let playerNameColumn = "playerName[" + positionObject + "]";
let playerCivicBoxColumn = "civIcon[" + positionObject + "]";
let playerCounterValue = "valueData[" + positionObject + "]";
if (playerState.team != -1)
{
rowPlayer = "playerBoxt[" + playerState.team + "][" + positionObject + "]";
playerOutcome = "playerOutcomet[" + playerState.team + "][" + positionObject + "]";
playerNameColumn = "playerNamet[" + playerState.team + "][" + positionObject + "]";
playerCivicBoxColumn = "civIcont[" + playerState.team + "][" + positionObject + "]";
playerCounterValue = "valueDataTeam[" + playerState.team + "][" + positionObject + "]";
}
let colorString = "color: " +
Math.floor(playerState.color.r * 255) + " " +
Math.floor(playerState.color.g * 255) + " " +
Math.floor(playerState.color.b * 255);
let rowPlayerObject = Engine.GetGUIObjectByName(rowPlayer);
rowPlayerObject.hidden = false;
rowPlayerObject.sprite = colorString + " " + g_PlayerBoxAlpha;
let boxSize = rowPlayerObject.size;
boxSize.right = rowPlayerObjectWidth;
rowPlayerObject.size = boxSize;
setOutcomeIcon(playerState.state, playerOutcome);
playerNameColumn = Engine.GetGUIObjectByName(playerNameColumn);
playerNameColumn.caption = g_GameData.sim.playerStates[i + 1].name;
playerNameColumn.tooltip = translateAISettings(g_GameData.sim.mapSettings.PlayerData[i + 1]);
let civIcon = Engine.GetGUIObjectByName(playerCivicBoxColumn);
civIcon.sprite = "stretched:" + g_CivData[playerState.civ].Emblem;
civIcon.tooltip = g_CivData[playerState.civ].Name;
updateCountersPlayer(playerState, panelInfo.counters, panelInfo.headings, playerCounterValue, index);
}
let teamCounterFn = panelInfo.teamCounterFn;
if (g_Teams && teamCounterFn)
updateCountersTeam(teamCounterFn, panelInfo.counters, panelInfo.headings, index);
}
function continueButton()
{
let summarySelectedData = {
"panel": g_SelectedPanel,
"charts": g_SelectedChart
};
if (g_GameData.gui.isInGame)
- Engine.PopGuiPageCB({
+ Engine.PopGuiPage({
"explicitResume": 0,
"summarySelectedData": summarySelectedData
});
else if (g_GameData.gui.dialog)
Engine.PopGuiPage();
else if (Engine.HasXmppClient())
Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false });
else if (g_GameData.gui.isReplay)
Engine.SwitchGuiPage("page_replaymenu.xml", {
"replaySelectionData": g_GameData.gui.replaySelectionData,
"summarySelectedData": summarySelectedData
});
else
Engine.SwitchGuiPage("page_pregame.xml");
}
function startReplay()
{
if (!Engine.StartVisualReplay(g_GameData.gui.replayDirectory))
{
warn("Replay file not found!");
return;
}
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": Engine.GetReplayAttributes(g_GameData.gui.replayDirectory),
"playerAssignments": {
"local": {
"name": singleplayerName(),
"player": -1
}
},
"savedGUIData": "",
"isReplay": true,
"replaySelectionData": g_GameData.gui.replaySelectionData
});
}
function initGUILabels()
{
let assignedState = g_GameData.sim.playerStates[g_GameData.gui.assignedPlayer || -1];
Engine.GetGUIObjectByName("summaryText").caption =
g_GameData.gui.isInGame ?
translate("Current Scores") :
g_GameData.gui.isReplay ?
translate("Scores at the end of the game.") :
g_GameData.gui.disconnected ?
translate("You have been disconnected.") :
!assignedState ?
translate("You have left the game.") :
assignedState.state == "won" ?
translate("You have won the battle!") :
assignedState.state == "defeated" ?
translate("You have been defeated…") :
translate("You have abandoned the game.");
Engine.GetGUIObjectByName("timeElapsed").caption = sprintf(
translate("Game time elapsed: %(time)s"), {
"time": timeToString(g_GameData.sim.timeElapsed)
});
let mapType = g_Settings.MapTypes.find(type => type.Name == g_GameData.sim.mapSettings.mapType);
let mapSize = g_Settings.MapSizes.find(size => size.Tiles == g_GameData.sim.mapSettings.Size || 0);
Engine.GetGUIObjectByName("mapName").caption = sprintf(
translate("%(mapName)s - %(mapType)s"), {
"mapName": translate(g_GameData.sim.mapSettings.Name),
"mapType": mapSize ? mapSize.Name : (mapType ? mapType.Title : "")
});
}
function initGUIButtons()
{
let replayButton = Engine.GetGUIObjectByName("replayButton");
replayButton.hidden = g_GameData.gui.isInGame || !g_GameData.gui.replayDirectory;
let lobbyButton = Engine.GetGUIObjectByName("lobbyButton");
lobbyButton.tooltip = colorizeHotkey(translate("%(hotkey)s: Toggle the multiplayer lobby in a dialog window."), "lobby");
lobbyButton.hidden = g_GameData.gui.isInGame || !Engine.HasXmppClient();
// Right-align lobby button
let lobbyButtonSize = lobbyButton.size;
let lobbyButtonWidth = lobbyButtonSize.right - lobbyButtonSize.left;
lobbyButtonSize.right = (replayButton.hidden ? Engine.GetGUIObjectByName("continueButton").size.left : replayButton.size.left) - 10;
lobbyButtonSize.left = lobbyButtonSize.right - lobbyButtonWidth;
lobbyButton.size = lobbyButtonSize;
}
function initTeamData()
{
// Panels
g_PlayerCount = g_GameData.sim.playerStates.length - 1;
if (g_GameData.sim.mapSettings.LockTeams)
{
// Count teams
for (let player = 1; player <= g_PlayerCount; ++player)
{
let playerTeam = g_GameData.sim.playerStates[player].team;
if (!g_Teams[playerTeam])
g_Teams[playerTeam] = [];
g_Teams[playerTeam].push(player);
}
if (g_Teams.every(team => team && team.length < 2))
g_Teams = false; // Each player has his own team. Displaying teams makes no sense.
}
else
g_Teams = false;
// Erase teams data if teams are not displayed
if (!g_Teams)
for (let p = 0; p < g_PlayerCount; ++p)
g_GameData.sim.playerStates[p+1].team = -1;
}
Index: ps/trunk/source/gui/GUIManager.h
===================================================================
--- ps/trunk/source/gui/GUIManager.h (revision 22675)
+++ ps/trunk/source/gui/GUIManager.h (revision 22676)
@@ -1,172 +1,196 @@
/* Copyright (C) 2019 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 .
*/
#ifndef INCLUDED_GUIMANAGER
#define INCLUDED_GUIMANAGER
#include
#include
#include "lib/input.h"
#include "lib/file/vfs/vfs_path.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/TemplateLoader.h"
#include "scriptinterface/ScriptVal.h"
#include "scriptinterface/ScriptInterface.h"
class CGUI;
class JSObject;
class IGUIObject;
struct CGUIColor;
struct SGUIIcon;
/**
* External interface to the GUI system.
*
* The GUI consists of a set of pages. Each page is constructed from a
* series of XML files, and is independent from any other page.
* Only one page is active at a time. All events and render requests etc
* will go to the active page. This lets the GUI switch between pre-game menu
* and in-game UI.
*/
class CGUIManager
{
NONCOPYABLE(CGUIManager);
public:
CGUIManager();
~CGUIManager();
shared_ptr GetScriptInterface()
{
return m_ScriptInterface;
}
shared_ptr GetRuntime() { return m_ScriptRuntime; }
shared_ptr GetActiveGUI() { return top(); }
/**
* Returns whether there are any current pages.
*/
bool HasPages();
/**
* Load a new GUI page and make it active. All current pages will be destroyed.
*/
void SwitchPage(const CStrW& name, ScriptInterface* srcScriptInterface, JS::HandleValue initData);
/**
* Load a new GUI page and make it active. All current pages will be retained,
* and will still be drawn and receive tick events, but will not receive
* user inputs.
+ * If given, the callbackHandler function will be executed once this page is closed.
*/
- void PushPage(const CStrW& pageName, shared_ptr initData);
+ void PushPage(const CStrW& pageName, shared_ptr initData, JS::HandleValue callbackFunc);
/**
* Unload the currently active GUI page, and make the previous page active.
* (There must be at least two pages when you call this.)
*/
- void PopPage();
- void PopPageCB(shared_ptr args);
+ void PopPage(shared_ptr args);
/**
* Called when a file has been modified, to hotload changes.
*/
Status ReloadChangedFile(const VfsPath& path);
/**
* Sets the default mouse pointer.
*/
void ResetCursor();
/**
* Called when we should reload all pages (e.g. translation hotloading update).
*/
Status ReloadAllPages();
/**
* Pass input events to the currently active GUI page.
*/
InReaction HandleEvent(const SDL_Event_* ev);
/**
* See CGUI::SendEventToAll; applies to the currently active page.
*/
void SendEventToAll(const CStr& eventName) const;
void SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const;
/**
* See CGUI::TickObjects; applies to @em all loaded pages.
*/
void TickObjects();
/**
* See CGUI::Draw; applies to @em all loaded pages.
*/
void Draw();
/**
* See CGUI::UpdateResolution; applies to @em all loaded pages.
*/
void UpdateResolution();
/**
* Calls the current page's script function getSavedGameData() and returns the result.
*/
std::string GetSavedGameData();
void RestoreSavedGameData(const std::string& jsonData);
/**
* Check if a template with this name exists
*/
bool TemplateExists(const std::string& templateName) const;
/**
* Retrieve the requested template, used for displaying faction specificities.
*/
const CParamNode& GetTemplate(const std::string& templateName);
private:
struct SGUIPage
{
// COPYABLE, because event handlers may invalidate page stack iterators by open or close pages,
// and event handlers need to be called for the entire stack.
+
+ /**
+ * Initializes the data that will be used to create the CGUI page one or multiple times (hotloading).
+ */
SGUIPage(const CStrW& pageName, const shared_ptr initData);
+
+ /**
+ * Create the CGUI with it's own ScriptInterface. Deletes the previous CGUI if it existed.
+ */
void LoadPage(shared_ptr scriptRuntime);
+ /**
+ * Sets the callback handler when a new page is opened that will be performed when the page is closed.
+ */
+ void SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc);
+
+ /**
+ * Execute the stored callback function with the given arguments.
+ */
+ void PerformCallbackFunction(shared_ptr args);
+
CStrW name;
boost::unordered_set inputs; // for hotloading
shared_ptr initData; // data to be passed to the init() function
shared_ptr gui; // the actual GUI page
+
+ /**
+ * Function executed by this parent GUI page when the child GUI page it pushed is popped.
+ * Notice that storing it in the SGUIPage instead of CGUI means that it will survive the hotloading CGUI reset.
+ */
+ shared_ptr callbackFunction;
};
shared_ptr top() const;
shared_ptr m_ScriptRuntime;
shared_ptr m_ScriptInterface;
typedef std::vector PageStackType;
PageStackType m_PageStack;
CTemplateLoader m_TemplateLoader;
};
extern CGUIManager* g_GUI;
extern InReaction gui_handler(const SDL_Event_* ev);
#endif // INCLUDED_GUIMANAGER
Index: ps/trunk/source/gui/scripting/JSInterface_GUIManager.h
===================================================================
--- ps/trunk/source/gui/scripting/JSInterface_GUIManager.h (revision 22675)
+++ ps/trunk/source/gui/scripting/JSInterface_GUIManager.h (revision 22676)
@@ -1,39 +1,38 @@
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_JSI_GUIMANAGER
#define INCLUDED_JSI_GUIMANAGER
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/system/ParamNode.h"
namespace JSI_GUIManager
{
- void PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData);
+ void PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction);
void SwitchGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData);
- void PopGuiPage(ScriptInterface::CxPrivate* pCxPrivate);
- void PopGuiPageCB(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args);
+ void PopGuiPage(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args);
JS::Value GetGUIObjectByName(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name);
std::wstring SetCursor(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name);
void ResetCursor(ScriptInterface::CxPrivate* pCxPrivate);
bool TemplateExists(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName);
CParamNode GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName);
void RegisterScriptFunctions(const ScriptInterface& scriptInterface);
}
#endif // INCLUDED_JSI_GUIMANAGER