Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js (revision 23943) +++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js (revision 23944) @@ -1,157 +1,157 @@ /** * These classes construct a chat message from simulation events initiated from the GuiInterface PushNotification method. */ class ChatMessageFormatSimulation { } ChatMessageFormatSimulation.attack = class { parse(msg) { if (msg.player != g_ViewedPlayer) return ""; let message = msg.targetIsDomesticAnimal ? translate("Your livestock has been attacked by %(attacker)s!") : translate("You have been attacked by %(attacker)s!"); return sprintf(message, { "attacker": colorizePlayernameByID(msg.attacker) }); } }; ChatMessageFormatSimulation.barter = class { parse(msg) { if (!g_IsObserver || Engine.ConfigDB_GetValue("user", "gui.session.notifications.barter") != "true") return ""; - let amountsSold = {}; - amountsSold[msg.resourceSold] = msg.amountsSold; + let amountGiven = {}; + amountGiven[msg.resourceGiven] = msg.amountGiven; - let amountsBought = {}; - amountsBought[msg.resourceBought] = msg.amountsBought; + let amountGained = {}; + amountGained[msg.resourceGained] = msg.amountGained; - return sprintf(translate("%(player)s bartered %(amountsBought)s for %(amountsSold)s."), { + return sprintf(translate("%(player)s bartered %(amountGiven)s for %(amountGained)s."), { "player": colorizePlayernameByID(msg.player), - "amountsBought": getLocalizedResourceAmounts(amountsBought), - "amountsSold": getLocalizedResourceAmounts(amountsSold) + "amountGiven": getLocalizedResourceAmounts(amountGiven), + "amountGained": getLocalizedResourceAmounts(amountGained) }); } }; ChatMessageFormatSimulation.diplomacy = class { parse(msg) { let messageType; if (g_IsObserver) messageType = "observer"; else if (Engine.GetPlayerID() == msg.sourcePlayer) messageType = "active"; else if (Engine.GetPlayerID() == msg.targetPlayer) messageType = "passive"; else return ""; return sprintf(translate(this.strings[messageType][msg.status]), { "player": colorizePlayernameByID(messageType == "active" ? msg.targetPlayer : msg.sourcePlayer), "player2": colorizePlayernameByID(messageType == "active" ? msg.sourcePlayer : msg.targetPlayer) }); } }; ChatMessageFormatSimulation.diplomacy.prototype.strings = { "active": { "ally": markForTranslation("You are now allied with %(player)s."), "enemy": markForTranslation("You are now at war with %(player)s."), "neutral": markForTranslation("You are now neutral with %(player)s.") }, "passive": { "ally": markForTranslation("%(player)s is now allied with you."), "enemy": markForTranslation("%(player)s is now at war with you."), "neutral": markForTranslation("%(player)s is now neutral with you.") }, "observer": { "ally": markForTranslation("%(player)s is now allied with %(player2)s."), "enemy": markForTranslation("%(player)s is now at war with %(player2)s."), "neutral": markForTranslation("%(player)s is now neutral with %(player2)s.") } }; ChatMessageFormatSimulation.phase = class { parse(msg) { let notifyPhase = Engine.ConfigDB_GetValue("user", "gui.session.notifications.phase"); if (notifyPhase == "none" || msg.player != g_ViewedPlayer && !g_IsObserver && !g_Players[msg.player].isMutualAlly[g_ViewedPlayer]) return ""; let message = ""; if (notifyPhase == "all") { if (msg.phaseState == "started") message = translate("%(player)s is advancing to the %(phaseName)s."); else if (msg.phaseState == "aborted") message = translate("The %(phaseName)s of %(player)s has been aborted."); } if (msg.phaseState == "completed") message = translate("%(player)s has reached the %(phaseName)s."); return sprintf(message, { "player": colorizePlayernameByID(msg.player), "phaseName": getEntityNames(GetTechnologyData(msg.phaseName, g_Players[msg.player].civ)) }); } }; ChatMessageFormatSimulation.playerstate = class { parse(msg) { if (!msg.message.pluralMessage) return sprintf(translate(msg.message), { "player": colorizePlayernameByID(msg.players[0]) }); let mPlayers = msg.players.map(playerID => colorizePlayernameByID(playerID)); let lastPlayer = mPlayers.pop(); return sprintf(translatePlural(msg.message.message, msg.message.pluralMessage, msg.message.pluralCount), { // Translation: This comma is used for separating first to penultimate elements in an enumeration. "players": mPlayers.join(translate(", ")), "lastPlayer": lastPlayer }); } }; /** * Optionally show all tributes sent in observer mode and tributes sent between allied players. * Otherwise, only show tributes sent directly to us, and tributes that we send. */ ChatMessageFormatSimulation.tribute = class { parse(msg) { let message = ""; if (msg.targetPlayer == Engine.GetPlayerID()) message = translate("%(player)s has sent you %(amounts)s."); else if (msg.sourcePlayer == Engine.GetPlayerID()) message = translate("You have sent %(player2)s %(amounts)s."); else if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.tribute") == "true" && (g_IsObserver || g_GameAttributes.settings.LockTeams && g_Players[msg.sourcePlayer].isMutualAlly[Engine.GetPlayerID()] && g_Players[msg.targetPlayer].isMutualAlly[Engine.GetPlayerID()])) message = translate("%(player)s has sent %(player2)s %(amounts)s."); return sprintf(message, { "player": colorizePlayernameByID(msg.sourcePlayer), "player2": colorizePlayernameByID(msg.targetPlayer), "amounts": getLocalizedResourceAmounts(msg.amounts) }); } }; Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 23943) +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 23944) @@ -1,556 +1,556 @@ /** * All tutorial messages received so far. */ var g_TutorialMessages = []; /** * GUI tags applied to the most recent tutorial message. */ var g_TutorialNewMessageTags = { "color": "yellow" }; /** * These handlers are called everytime a client joins or disconnects. */ var g_PlayerAssignmentsChangeHandlers = new Set(); /** * These handlers are called when the ceasefire time has run out. */ var g_CeasefireEndedHandlers = new Set(); /** * These handlers are fired when the match is networked and * the current client established the connection, authenticated, * finished the loading screen, starts or finished synchronizing after a rejoin. * The messages are constructed in NetClient.cpp. */ var g_NetworkStatusChangeHandlers = new Set(); /** * These handlers are triggered whenever a client finishes the loading screen. */ var g_ClientsLoadingHandlers = new Set(); /** * These handlers are fired if the server informed the players that the networked game is out of sync. */ var g_NetworkOutOfSyncHandlers = new Set(); /** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { "netstatus": msg => { handleNetStatusMessage(msg); }, "netwarn": msg => { addNetworkWarning(msg); }, "out-of-sync": msg => { for (let handler of g_NetworkOutOfSyncHandlers) handler(msg); }, "players": msg => { handlePlayerAssignmentsMessage(msg); }, "paused": msg => { g_PauseControl.setClientPauseState(msg.guid, msg.pause); }, "clients-loading": msg => { for (let handler of g_ClientsLoadingHandlers) handler(msg.guids); }, "rejoined": msg => { addChatMessage({ "type": "rejoined", "guid": msg.guid }); }, "kicked": msg => { addChatMessage({ "type": "kicked", "username": msg.username, "banned": msg.banned }); }, "chat": msg => { addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text }); }, "gamesetup": msg => {}, // Needed for autostart "start": msg => {} }; var g_PlayerStateMessages = { "won": translate("You have won!"), "defeated": translate("You have been defeated!") }; /** * Defines how the GUI reacts to notifications that are sent by the simulation. * Don't open new pages (message boxes) here! Otherwise further notifications * handled in the same turn can't access the GUI objects anymore. */ var g_NotificationsTypes = { "aichat": function(notification, player) { let message = { "type": "message", "text": notification.message, "guid": findGuidForPlayerID(player) || -1, "player": player, "translate": true }; if (notification.translateParameters) { message.translateParameters = notification.translateParameters; message.parameters = notification.parameters; colorizePlayernameParameters(notification.parameters); } addChatMessage(message); }, "defeat": function(notification, player) { playersFinished(notification.allies, notification.message, false); }, "won": function(notification, player) { playersFinished(notification.allies, notification.message, true); }, "diplomacy": function(notification, player) { updatePlayerData(); g_DiplomacyColors.onDiplomacyChange(); addChatMessage({ "type": "diplomacy", "sourcePlayer": player, "targetPlayer": notification.targetPlayer, "status": notification.status }); }, "ceasefire-ended": function(notification, player) { updatePlayerData(); for (let handler of g_CeasefireEndedHandlers) handler(); }, "tutorial": function(notification, player) { updateTutorial(notification); }, "tribute": function(notification, player) { addChatMessage({ "type": "tribute", "sourcePlayer": notification.donator, "targetPlayer": player, "amounts": notification.amounts }); }, "barter": function(notification, player) { addChatMessage({ "type": "barter", "player": player, - "amountsSold": notification.amountsSold, - "amountsBought": notification.amountsBought, - "resourceSold": notification.resourceSold, - "resourceBought": notification.resourceBought + "amountGiven": notification.amountGiven, + "amountGained": notification.amountGained, + "resourceGiven": notification.resourceGiven, + "resourceGained": notification.resourceGained }); }, "spy-response": function(notification, player) { g_DiplomacyDialog.onSpyResponse(notification, player); if (notification.entity && g_ViewedPlayer == player) { g_DiplomacyDialog.close(); setCameraFollow(notification.entity); } }, "attack": function(notification, player) { if (player != g_ViewedPlayer) return; // Focus camera on attacks if (g_FollowPlayer) { setCameraFollow(notification.target); g_Selection.reset(); if (notification.target) g_Selection.addList([notification.target]); } if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.attack") !== "true") return; addChatMessage({ "type": "attack", "player": player, "attacker": notification.attacker, "targetIsDomesticAnimal": notification.targetIsDomesticAnimal }); }, "phase": function(notification, player) { addChatMessage({ "type": "phase", "player": player, "phaseName": notification.phaseName, "phaseState": notification.phaseState }); }, "dialog": function(notification, player) { if (player == Engine.GetPlayerID()) openDialog(notification.dialogName, notification.data, player); }, "playercommand": function(notification, player) { // For observers, focus the camera on units commanded by the selected player if (!g_FollowPlayer || player != g_ViewedPlayer) return; let cmd = notification.cmd; // Ignore rallypoint commands of trained animals let entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]); if (g_ViewedPlayer != 0 && entState && entState.identity && entState.identity.classes && entState.identity.classes.indexOf("Animal") != -1) return; // Focus the structure to build. if (cmd.type == "repair") { let targetState = GetEntityState(cmd.target); if (targetState) Engine.CameraMoveTo(targetState.position.x, targetState.position.z); } else if (cmd.type == "delete-entities" && notification.position) Engine.CameraMoveTo(notification.position.x, notification.position.y); // Focus commanded entities, but don't lose previous focus when training units else if (cmd.type != "train" && cmd.type != "research" && entState) setCameraFollow(cmd.entities[0]); if (["walk", "attack-walk", "patrol"].indexOf(cmd.type) != -1) DrawTargetMarker(cmd); // Select units affected by that command let selection = []; if (cmd.entities) selection = cmd.entities; if (cmd.target) selection.push(cmd.target); // Allow gaia in selection when gathering g_Selection.reset(); g_Selection.addList(selection, false, cmd.type == "gather"); }, "play-tracks": function(notification, player) { if (notification.lock) { global.music.storeTracks(notification.tracks.map(track => ({ "Type": "custom", "File": track }))); global.music.setState(global.music.states.CUSTOM); } global.music.setLocked(notification.lock); } }; function registerPlayerAssignmentsChangeHandler(handler) { g_PlayerAssignmentsChangeHandlers.add(handler); } function registerCeasefireEndedHandler(handler) { g_CeasefireEndedHandlers.add(handler); } function registerNetworkOutOfSyncHandler(handler) { g_NetworkOutOfSyncHandlers.add(handler); } function registerNetworkStatusChangeHandler(handler) { g_NetworkStatusChangeHandlers.add(handler); } function registerClientsLoadingHandler(handler) { g_ClientsLoadingHandlers.add(handler); } function findGuidForPlayerID(playerID) { return Object.keys(g_PlayerAssignments).find(guid => g_PlayerAssignments[guid].player == playerID); } /** * Processes all pending notifications sent from the GUIInterface simulation component. */ function handleNotifications() { for (let notification of Engine.GuiInterfaceCall("GetNotifications")) { if (!notification.players || !notification.type || !g_NotificationsTypes[notification.type]) { error("Invalid GUI notification: " + uneval(notification)); continue; } for (let player of notification.players) g_NotificationsTypes[notification.type](notification, player); } } function toggleTutorial() { let tutorialPanel = Engine.GetGUIObjectByName("tutorialPanel"); tutorialPanel.hidden = !tutorialPanel.hidden || !Engine.GetGUIObjectByName("tutorialText").caption; } /** * Updates the tutorial panel when a new goal. */ function updateTutorial(notification) { // Show the tutorial panel if not yet done Engine.GetGUIObjectByName("tutorialPanel").hidden = false; if (notification.warning) { Engine.GetGUIObjectByName("tutorialWarning").caption = coloredText(translate(notification.warning), "orange"); return; } let notificationText = notification.instructions.reduce((instructions, item) => instructions + (typeof item == "string" ? translate(item) : colorizeHotkey(translate(item.text), item.hotkey)), ""); Engine.GetGUIObjectByName("tutorialText").caption = g_TutorialMessages.concat(setStringTags(notificationText, g_TutorialNewMessageTags)).join("\n"); g_TutorialMessages.push(notificationText); if (notification.readyButton) { Engine.GetGUIObjectByName("tutorialReady").hidden = false; if (notification.leave) { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click to quit this tutorial."); Engine.GetGUIObjectByName("tutorialReady").caption = translate("Quit"); Engine.GetGUIObjectByName("tutorialReady").onPress = endGame; } else Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click when ready."); } else { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Follow the instructions."); Engine.GetGUIObjectByName("tutorialReady").hidden = true; } } /** * Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. * Saves the received object to mainlog.html. */ function handleNetMessages() { while (true) { let msg = Engine.PollNetworkClient(); if (!msg) return; log("Net message: " + uneval(msg)); if (g_NetMessageTypes[msg.type]) g_NetMessageTypes[msg.type](msg); else error("Unrecognised net message type '" + msg.type + "'"); } } function handleNetStatusMessage(message) { if (g_Disconnected) return; g_IsNetworkedActive = message.status == "active"; if (message.status == "disconnected") { g_Disconnected = true; updateCinemaPath(); closeOpenDialogs(); } for (let handler of g_NetworkStatusChangeHandlers) handler(message); } function handlePlayerAssignmentsMessage(message) { for (let guid in g_PlayerAssignments) if (!message.newAssignments[guid]) onClientLeave(guid); let joins = Object.keys(message.newAssignments).filter(guid => !g_PlayerAssignments[guid]); g_PlayerAssignments = message.newAssignments; joins.forEach(guid => { onClientJoin(guid); }); for (let handler of g_PlayerAssignmentsChangeHandlers) handler(); // TODO: use subscription instead updateGUIObjects(); } function onClientJoin(guid) { let playerID = g_PlayerAssignments[guid].player; if (g_Players[playerID]) { g_Players[playerID].guid = guid; g_Players[playerID].name = g_PlayerAssignments[guid].name; g_Players[playerID].offline = false; } addChatMessage({ "type": "connect", "guid": guid }); } function onClientLeave(guid) { g_PauseControl.setClientPauseState(guid, false); for (let id in g_Players) if (g_Players[id].guid == guid) g_Players[id].offline = true; addChatMessage({ "type": "disconnect", "guid": guid }); } function addChatMessage(msg) { g_Chat.ChatMessageHandler.handleMessage(msg); } function clearChatMessages() { g_Chat.ChatOverlay.clearChatMessages(); } /** * This function is used for AIs, whose names don't exist in g_PlayerAssignments. */ function colorizePlayernameByID(playerID) { let username = g_Players[playerID] && escapeText(g_Players[playerID].name); return colorizePlayernameHelper(username, playerID); } function colorizePlayernameByGUID(guid) { let username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : ""; let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1; return colorizePlayernameHelper(username, playerID); } function colorizePlayernameHelper(username, playerID) { let playerColor = playerID > -1 ? g_DiplomacyColors.getPlayerColor(playerID) : "white"; return coloredText(username || translate("Unknown Player"), playerColor); } /** * Insert the colorized playername to chat messages sent by the AI and time notifications. */ function colorizePlayernameParameters(parameters) { for (let param in parameters) if (param.startsWith("_player_")) parameters[param] = colorizePlayernameByID(parameters[param]); } /** * Custom dialog response handling, usable by trigger maps. */ function sendDialogAnswer(guiObject, dialogName) { Engine.GetGUIObjectByName(dialogName + "-dialog").hidden = true; Engine.PostNetworkCommand({ "type": "dialog-answer", "dialog": dialogName, "answer": guiObject.name.split("-").pop(), }); resumeGame(); } /** * Custom dialog opening, usable by trigger maps. */ function openDialog(dialogName, data, player) { let dialog = Engine.GetGUIObjectByName(dialogName + "-dialog"); if (!dialog) { warn("messages.js: Unknow dialog with name " + dialogName); return; } dialog.hidden = false; for (let objName in data) { let obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName); if (!obj) { warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog."); continue; } for (let key in data[objName]) { let n = data[objName][key]; if (typeof n == "object" && n.message) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); obj[key] = sprintf(message, parameters); } else obj[key] = n; } } g_PauseControl.implicitPause(); } Index: ps/trunk/binaries/data/mods/public/simulation/components/Barter.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Barter.js (revision 23943) +++ ps/trunk/binaries/data/mods/public/simulation/components/Barter.js (revision 23944) @@ -1,158 +1,158 @@ function Barter() {} Barter.prototype.Schema = ""; /** * The "true price" is a base price of 100 units of resource (for the case of some resources being of more worth than others). * With current bartering system only relative values makes sense so if for example stone is two times more expensive than wood, * there will 2:1 exchange rate. * * Constant part of price percentage difference between true price and buy/sell price. * Buy price equal to true price plus constant difference. * Sell price equal to true price minus constant difference. */ Barter.prototype.CONSTANT_DIFFERENCE = 10; /** * Additional difference of prices in percents, added after each deal to specified resource price. */ Barter.prototype.DIFFERENCE_PER_DEAL = 2; /** * Price difference percentage which restored each restore timer tick */ Barter.prototype.DIFFERENCE_RESTORE = 0.5; /** * Interval of timer which slowly restore prices after deals */ Barter.prototype.RESTORE_TIMER_INTERVAL = 5000; Barter.prototype.Init = function() { this.priceDifferences = {}; for (let resource of Resources.GetBarterableCodes()) this.priceDifferences[resource] = 0; this.restoreTimer = undefined; }; Barter.prototype.GetPrices = function(playerID) { let prices = { "buy": {}, "sell": {} }; let multiplier = QueryPlayerIDInterface(playerID).GetBarterMultiplier(); for (let resource of Resources.GetBarterableCodes()) { let truePrice = Resources.GetResource(resource).truePrice; prices.buy[resource] = truePrice * (100 + this.CONSTANT_DIFFERENCE + this.priceDifferences[resource]) * multiplier.buy[resource] / 100; prices.sell[resource] = truePrice * (100 - this.CONSTANT_DIFFERENCE + this.priceDifferences[resource]) * multiplier.sell[resource] / 100; } return prices; }; Barter.prototype.PlayerHasMarket = function(playerID) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var entities = cmpRangeManager.GetEntitiesByPlayer(playerID); for (var entity of entities) { var cmpFoundation = Engine.QueryInterface(entity, IID_Foundation); var cmpIdentity = Engine.QueryInterface(entity, IID_Identity); if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter")) return true; } return false; }; Barter.prototype.ExchangeResources = function(playerID, resourceToSell, resourceToBuy, amount) { if (amount <= 0) { warn("ExchangeResources: incorrect amount: " + uneval(amount)); return; } let availResources = Resources.GetBarterableCodes(); if (availResources.indexOf(resourceToSell) == -1) { warn("ExchangeResources: incorrect resource to sell: " + uneval(resourceToSell)); return; } if (availResources.indexOf(resourceToBuy) == -1) { warn("ExchangeResources: incorrect resource to buy: " + uneval(resourceToBuy)); return; } // This can occur when the player issues the order just before the market is destroyed or captured if (!this.PlayerHasMarket(playerID)) return; if (amount != 100 && amount != 500) return; var cmpPlayer = QueryPlayerIDInterface(playerID); var prices = this.GetPrices(playerID); var amountsToSubtract = {}; amountsToSubtract[resourceToSell] = amount; if (cmpPlayer.TrySubtractResources(amountsToSubtract)) { var amountToAdd = Math.round(prices["sell"][resourceToSell] / prices["buy"][resourceToBuy] * amount); cmpPlayer.AddResource(resourceToBuy, amountToAdd); - // Display chat message to observers + // Display chat message to observers. var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGUIInterface) cmpGUIInterface.PushNotification({ "type": "barter", "players": [playerID], - "amountsSold": amount, - "amountsBought": amountToAdd, - "resourceSold": resourceToSell, - "resourceBought": resourceToBuy + "amountGiven": amount, + "amountGained": amountToAdd, + "resourceGiven": resourceToSell, + "resourceGained": resourceToBuy }); var cmpStatisticsTracker = QueryPlayerIDInterface(playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) { cmpStatisticsTracker.IncreaseResourcesSoldCounter(resourceToSell, amount); cmpStatisticsTracker.IncreaseResourcesBoughtCounter(resourceToBuy, amountToAdd); } let difference = this.DIFFERENCE_PER_DEAL * amount / 100; // Increase price difference for both exchange resources. // Overall price difference (dynamic +/- constant) can't exceed +-99%. this.priceDifferences[resourceToSell] -= difference; this.priceDifferences[resourceToSell] = Math.min(99 - this.CONSTANT_DIFFERENCE, Math.max(this.CONSTANT_DIFFERENCE - 99, this.priceDifferences[resourceToSell])); this.priceDifferences[resourceToBuy] += difference; this.priceDifferences[resourceToBuy] = Math.min(99 - this.CONSTANT_DIFFERENCE, Math.max(this.CONSTANT_DIFFERENCE - 99, this.priceDifferences[resourceToBuy])); } if (this.restoreTimer === undefined) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.restoreTimer = cmpTimer.SetInterval(this.entity, IID_Barter, "ProgressTimeout", this.RESTORE_TIMER_INTERVAL, this.RESTORE_TIMER_INTERVAL, {}); } }; Barter.prototype.ProgressTimeout = function(data) { let needRestore = false; for (let resource of Resources.GetBarterableCodes()) { // Calculate value to restore, it should be limited to [-DIFFERENCE_RESTORE; DIFFERENCE_RESTORE] interval let differenceRestore = Math.min(this.DIFFERENCE_RESTORE, Math.max(-this.DIFFERENCE_RESTORE, this.priceDifferences[resource])); differenceRestore = -differenceRestore; this.priceDifferences[resource] += differenceRestore; // If price difference still exists then set flag to run timer again if (this.priceDifferences[resource] != 0) needRestore = true; } if (!needRestore) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.restoreTimer); this.restoreTimer = undefined; } }; Engine.RegisterSystemComponentType(IID_Barter, "Barter", Barter);