petui / Tiberium Alliances Real POI Bonus

// ==UserScript==
// @name           Tiberium Alliances Real POI Bonus
// @version        1.0.1
// @namespace      https://openuserjs.org/users/petui
// @license        GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @author         petui
// @description    Displays actual gain/loss for POIs by taking rank multiplier properly into account
// @include        http*://prodgame*.alliances.commandandconquer.com/*/index.aspx*
// @updateURL      https://openuserjs.org/meta/petui/Tiberium_Alliances_Real_POI_Bonus.meta.js
// ==/UserScript==
'use strict';

(function() {
	var main = function() {
		'use strict';

		function createRealPOIBonus() {
			console.log('RealPOIBonus loaded');

			qx.Class.define('RealPOIBonus', {
				type: 'singleton',
				extend: qx.core.Object,
				statics: {
					PoiTypeToPoiRankingTypeMap: {},
					PoiRankingTypeToSortColumnMap: {}
				},
				defer: function(statics) {
					statics.PoiTypeToPoiRankingTypeMap[ClientLib.Base.EPOIType.TiberiumBonus] = ClientLib.Data.Ranking.ERankingType.BonusTiberium;
					statics.PoiTypeToPoiRankingTypeMap[ClientLib.Base.EPOIType.CrystalBonus] = ClientLib.Data.Ranking.ERankingType.BonusCrystal;
					statics.PoiTypeToPoiRankingTypeMap[ClientLib.Base.EPOIType.PowerBonus] = ClientLib.Data.Ranking.ERankingType.BonusPower;
					statics.PoiTypeToPoiRankingTypeMap[ClientLib.Base.EPOIType.InfanteryBonus] = ClientLib.Data.Ranking.ERankingType.BonusInfantry;
					statics.PoiTypeToPoiRankingTypeMap[ClientLib.Base.EPOIType.VehicleBonus] = ClientLib.Data.Ranking.ERankingType.BonusVehicles;
					statics.PoiTypeToPoiRankingTypeMap[ClientLib.Base.EPOIType.AirBonus] = ClientLib.Data.Ranking.ERankingType.BonusAircraft;
					statics.PoiTypeToPoiRankingTypeMap[ClientLib.Base.EPOIType.DefenseBonus] = ClientLib.Data.Ranking.ERankingType.BonusDefense;

					statics.PoiRankingTypeToSortColumnMap[ClientLib.Data.Ranking.ERankingType.BonusTiberium] = ClientLib.Data.Ranking.ESortColumn.TiberiumScore;
					statics.PoiRankingTypeToSortColumnMap[ClientLib.Data.Ranking.ERankingType.BonusCrystal] = ClientLib.Data.Ranking.ESortColumn.CrystalScore;
					statics.PoiRankingTypeToSortColumnMap[ClientLib.Data.Ranking.ERankingType.BonusPower] = ClientLib.Data.Ranking.ESortColumn.PowerScore;
					statics.PoiRankingTypeToSortColumnMap[ClientLib.Data.Ranking.ERankingType.BonusInfantry] = ClientLib.Data.Ranking.ESortColumn.InfantryScore;
					statics.PoiRankingTypeToSortColumnMap[ClientLib.Data.Ranking.ERankingType.BonusVehicles] = ClientLib.Data.Ranking.ESortColumn.VehicleScore;
					statics.PoiRankingTypeToSortColumnMap[ClientLib.Data.Ranking.ERankingType.BonusAircraft] = ClientLib.Data.Ranking.ESortColumn.AircraftScore;
					statics.PoiRankingTypeToSortColumnMap[ClientLib.Data.Ranking.ERankingType.BonusDefense] = ClientLib.Data.Ranking.ESortColumn.DefenseScore;
				},
				members: {
					rankingBonusDataCache: {},
					container: null,
					titleLabel: null,
					amountLabel: null,
					ownedPoiCount: 0,

					initialize: function() {
						this.initializeHacks();

						this.container = new qx.ui.container.Composite(new qx.ui.layout.HBox(4)).set({
							textColor: 'text-region-tooltip',
							marginRight: 10
						});
						this.container.add(this.titleLabel = new qx.ui.basic.Label());
						this.container.add(this.amountLabel = new qx.ui.basic.Label());

						var poiStatusInfo = webfrontend.gui.region.RegionPointOfInterestStatusInfo.getInstance();
						poiStatusInfo.getChildren()[0].addAt(this.container, 4);
						poiStatusInfo.addListener('appear', this.onStatusInfoAppear, this);

						phe.cnc.Util.attachNetEvent(ClientLib.Data.MainData.GetInstance().get_Alliance(), 'Change', ClientLib.Data.AllianceChange, this, this.onAllianceChange);
						this.onAllianceChange();
					},

					initializeHacks: function() {
						if (typeof webfrontend.gui.region.RegionPointOfInterestStatusInfo.prototype.getObject !== 'function') {
							var source = webfrontend.gui.region.RegionPointOfInterestStatusInfo.prototype.setObject.toString();
							var objectMemberName = source.match(/^function \(([A-Za-z]+)\)\{this\.([A-Za-z_]+)=\1;/)[2];

							/**
							 * @returns {ClientLib.Vis.Region.RegionPointOfInterest}
							 */
							webfrontend.gui.region.RegionPointOfInterestStatusInfo.prototype.getObject = function() {
								return this[objectMemberName];
							};
						}
					},

					onAllianceChange: function() {
						var alliance = ClientLib.Data.MainData.GetInstance().get_Alliance();
						var poiCount = alliance.get_Exists() ? alliance.get_OwnedPOIs().length : 0;

						if (poiCount !== this.ownedPoiCount) {
							this.ownedPoiCount = poiCount;
							this.rankingBonusDataCache = {};
						}
					},

					/**
					 * @param {qx.event.type.Event} event
					 */
					onStatusInfoAppear: function(event) {
						var visObject = event.getTarget().getObject();
						var allianceId = ClientLib.Data.MainData.GetInstance().get_Alliance().get_Id();

						if (allianceId > 0 && visObject.get_Type() !== ClientLib.Data.WorldSector.WorldObjectPointOfInterest.EPOIType.TunnelExit) {
							var selectedPoiScore = ClientLib.Base.PointOfInterestTypes.GetScoreByLevel(visObject.get_Level());
							var poiType = ClientLib.Base.PointOfInterestTypes.GetPOITypeFromWorldPOIType(visObject.get_Type());
							var poiRankScore = ClientLib.Data.MainData.GetInstance().get_Alliance().get_POIRankScore()[poiType - ClientLib.Base.EPOIType.RankedTypeBegin];
							var allianceRank = poiRankScore.r;
							var allianceScore = poiRankScore.s;
							var nextAllianceScore = poiRankScore.ns;
							var previousAllianceScore = poiRankScore.ps;
							var currentTotalBonus = ClientLib.Base.PointOfInterestTypes.GetTotalBonusByType(poiType, allianceRank, allianceScore);
							var gainOrLoss = null;

							if (visObject.get_OwnerAllianceId() === allianceId) {
								this.titleLabel.setValue('Real loss:');

								if (previousAllianceScore <= 0) {
									// No rank multiplier; no loss by rank
									gainOrLoss = currentTotalBonus - ClientLib.Base.PointOfInterestTypes.GetTotalBonusByType(poiType, allianceRank, allianceScore - selectedPoiScore);
								}
								else if (allianceScore - selectedPoiScore < previousAllianceScore) {
									// Falling behind previous alliance; need to use rankings
								}
								else {
									// No loss by rank; if we end up with same score as previous alliance, our rank stays the same and they get same rank
									gainOrLoss = currentTotalBonus - ClientLib.Base.PointOfInterestTypes.GetTotalBonusByType(poiType, allianceRank, allianceScore - selectedPoiScore);
								}
							}
							else {
								this.titleLabel.setValue('Real gain:');

								if (!allianceScore) {
									// Zero bonus; need to use rankings
								}
								else if (nextAllianceScore <= 0 || allianceRank <= 1) {
									// Already rank 1; no gain by rank
									gainOrLoss = ClientLib.Base.PointOfInterestTypes.GetTotalBonusByType(poiType, allianceRank, allianceScore + selectedPoiScore) - currentTotalBonus;
								}
								else if (visObject.get_OwnerAllianceId() !== webfrontend.gui.widgets.AllianceLabel.ESpecialNoAllianceName) {
									// Current owner of POI will lose score while we gain; need to use rankings
								}
								else if (allianceScore + selectedPoiScore > nextAllianceScore) {
									// Overtaking next alliance; need to use rankings
								}
								else if (allianceScore + selectedPoiScore < nextAllianceScore) {
									// No gain by rank
									gainOrLoss = ClientLib.Base.PointOfInterestTypes.GetTotalBonusByType(poiType, allianceRank, allianceScore + selectedPoiScore) - currentTotalBonus;
								}
								else {
									// Same score as next alliance; same rank and same bonus as them
									gainOrLoss = ClientLib.Base.PointOfInterestTypes.GetTotalBonusByType(poiType, allianceRank - 1, allianceScore + selectedPoiScore) - currentTotalBonus;
								}
							}

							if (gainOrLoss === null) {
								this.amountLabel.setValue('Loading...');
								this.fetchAndCalculateBonusWithRankingData(poiType, allianceRank, allianceScore, selectedPoiScore, allianceId, visObject.get_OwnerAllianceId());
							}
							else {
								this.amountLabel.setValue(this.formatGainOrLoss(gainOrLoss, poiType));
							}

							this.container.setVisibility('visible');
						}
						else {
							this.container.setVisibility('excluded');
						}
					},

					/**
					 * @param {ClientLib.Base.EPOIType} poiType
					 * @param {Number} currentRank
					 * @param {Number} currentScore
					 * @param {Number} poiScore
					 * @param {Number} allianceId
					 * @param {Number} poiOwnerId
					 */
					fetchAndCalculateBonusWithRankingData: function(poiType, currentRank, currentScore, poiScore, allianceId, poiOwnerId) {
						var context = {
							poiType: poiType,
							currentRank: currentRank,
							currentScore: currentScore,
							poiScore: poiScore,
							allianceId: allianceId,
							poiOwnerId: poiOwnerId
						};

						if (poiType in this.rankingBonusDataCache && this.rankingBonusDataCache[poiType].expire >= Date.now()) {
							this.calculateBonus(context, this.rankingBonusDataCache[poiType].results);
						}
						else {
							var lastMultiplierRank = Object.keys(ClientLib.Res.ResMain.GetInstance().GetGamedata().poibmbr).length;
							var rankingPoiType = RealPOIBonus.PoiTypeToPoiRankingTypeMap[poiType];
							var sortColumn = RealPOIBonus.PoiRankingTypeToSortColumnMap[rankingPoiType];

							ClientLib.Net.CommunicationManager.GetInstance().SendSimpleCommand('RankingGetData', {
								firstIndex: 0,
								lastIndex: lastMultiplierRank,
								view: ClientLib.Data.Ranking.EViewType.Alliance,
								rankingType: rankingPoiType,
								sortColumn: sortColumn,
								ascending: true
							}, phe.cnc.Util.createEventDelegate(ClientLib.Net.CommandResult, this, this.onRankingGetData), context);
						}
					},

					/**
					 * @param {Object} context
					 * @param {Object} results
					 */
					onRankingGetData: function(context, results) {
						if (results === null) {
							return;
						}

						var allianceBonuses = results.a;

						// Remove own alliance from list and add missing scores
						for (var i = 0; i < allianceBonuses.length; i++) {
							if (allianceBonuses[i].a === context.allianceId) {
								allianceBonuses.splice(i--, 1);
							}
							else if (allianceBonuses[i].pois === undefined) {
								allianceBonuses[i].pois = 0;
							}
						}

						this.rankingBonusDataCache[context.poiType] = {
							expire: Date.now() + 600000,
							results: allianceBonuses
						};

						this.calculateBonus(context, allianceBonuses);
					},

					/**
					 * @param {Object} context
					 * @param {Array} allianceBonuses
					 */
					calculateBonus: function(context, allianceBonuses) {
						var isGain = context.poiOwnerId !== context.allianceId;
						var i;

						if (isGain && context.poiOwnerId !== webfrontend.gui.widgets.AllianceLabel.ESpecialNoAllianceName) {
							// Subtract POI score from current owner
							for (i = 0; i < allianceBonuses.length; i++) {
								if (allianceBonuses[i].a === context.poiOwnerId) {
									// Array can be safely modified after cloning
									allianceBonuses = allianceBonuses.map(this.shallowClone);
									allianceBonuses[i].pois -= context.poiScore;

									allianceBonuses.sort(function(a, b) {
										return b.pois - a.pois;
									});
									break;
								}
							}
						}

						var newAllianceScore = context.currentScore + (isGain ? context.poiScore : -context.poiScore);

						for (i = 0; i < allianceBonuses.length; i++) {
							if (allianceBonuses[i].pois <= newAllianceScore) {
								break;
							}
						}

						var currentTotalBonus = ClientLib.Base.PointOfInterestTypes.GetTotalBonusByType(context.poiType, context.currentRank, context.currentScore);
						var newTotalBonus = ClientLib.Base.PointOfInterestTypes.GetTotalBonusByType(context.poiType, i + 1, newAllianceScore);
						var gainOrLoss = isGain
							? newTotalBonus - currentTotalBonus
							: currentTotalBonus - newTotalBonus;

						this.amountLabel.setValue(this.formatGainOrLoss(gainOrLoss, context.poiType));
					},

					/**
					 * @param {Number} gainOrLoss
					 * @param {ClientLib.Base.EPOIType} poiType
					 * @returns {String}
					 */
					formatGainOrLoss: function(gainOrLoss, poiType) {
						switch (poiType) {
							case ClientLib.Base.EPOIType.TiberiumBonus:
							case ClientLib.Base.EPOIType.CrystalBonus:
							case ClientLib.Base.EPOIType.PowerBonus:
								return phe.cnc.gui.util.Numbers.formatNumbers(gainOrLoss) + '/h';
							case ClientLib.Base.EPOIType.InfanteryBonus:
							case ClientLib.Base.EPOIType.VehicleBonus:
							case ClientLib.Base.EPOIType.AirBonus:
							case ClientLib.Base.EPOIType.DefenseBonus:
								return phe.cnc.gui.util.Numbers.formatNumbers(gainOrLoss) + '%';
						};
					},

					/**
					 * @param {Object} object
					 * @returns {Object}
					 */
					shallowClone: function(object) {
						var clone = new object.constructor;

						for (var key in object) {
							if (object.hasOwnProperty(key)) {
								clone[key] = object[key];
							}
						}

						return clone;
					}
				}
			});
		}

		function waitForGame() {
			try {
				if (typeof qx !== 'undefined' && qx.core.Init.getApplication() && qx.core.Init.getApplication().initDone) {
					createRealPOIBonus();
					RealPOIBonus.getInstance().initialize();
				}
				else {
					setTimeout(waitForGame, 1000);
				}
			}
			catch (e) {
				console.log('RealPOIBonus: ', e.toString());
			}
		}

		setTimeout(waitForGame, 1000);
	};

	var script = document.createElement('script');
	script.innerHTML = '(' + main.toString() + ')();';
	script.type = 'text/javascript';
	document.getElementsByTagName('head')[0].appendChild(script);
})();