Index: ps/trunk/binaries/data/mods/public/gui/summary/counters.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/summary/counters.js (revision 16932) +++ ps/trunk/binaries/data/mods/public/gui/summary/counters.js (revision 16933) @@ -1,347 +1,365 @@ // FUNCTIONS FOR CALCULATING SCORES var teamMiscHelperData = []; function resetDataHelpers() { teamMiscHelperData = []; } function updateCountersPlayer(playerState, counters, idGUI) { for (var w in counters) { var fn = counters[w].fn; Engine.GetGUIObjectByName(idGUI + "[" + w + "]").caption = fn && fn(playerState, w); } } function calculateEconomyScore(playerState, position) { let total = 0; for each (var res in playerState.statistics.resourcesGathered) total += res; return Math.round(total / 10); } function calculateMilitaryScore(playerState, position) { return Math.round((playerState.statistics.enemyUnitsKilledValue + playerState.statistics.enemyBuildingsDestroyedValue) / 10); } function calculateExplorationScore(playerState, position) { return playerState.statistics.percentMapExplored * 10; } function calculateScoreTotal(playerState, position) { return calculateEconomyScore(playerState) + calculateMilitaryScore(playerState) + calculateExplorationScore(playerState); } function calculateScoreTeam(counters) { for (var t in g_Teams) { if (t == -1) continue; for (var w in counters) { var total = 0; for (var p = 0; p < g_Teams[t]; ++p) total += (+Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + p + "][" + w + "]").caption); Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + w + "]").caption = total; } } } function calculateBuildings(playerState, position) { var type = BUILDINGS_TYPES[position]; return TRAINED_COLOR + playerState.statistics.buildingsConstructed[type] + '[/color] / ' + LOST_COLOR + playerState.statistics.buildingsLost[type] + '[/color] / ' + KILLED_COLOR + playerState.statistics.enemyBuildingsDestroyed[type] + '[/color]'; } function calculateColorsTeam(counters) { for (var t in g_Teams) { if (t == -1) continue; for (var w in counters) { var total = { c : 0, l : 0, d : 0 }; for (var p = 0; p < g_Teams[t]; ++p) { var caption = Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + p + "][" + w + "]").caption; // clean [Color=""], [/Color] and white space for make the sum more easy caption = caption.replace(/\[([\w\' \\\"\/\=]*)\]|\s/g, ""); var splitCaption = caption.split("/"); total.c += (+splitCaption[0]); total.l += (+splitCaption[1]); total.d += (+splitCaption[2]); } var teamTotal = TRAINED_COLOR + total.c + '[/color] / ' + LOST_COLOR + total.l + '[/color] / ' + KILLED_COLOR + total.d + '[/color]'; Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + w + "]").caption = teamTotal; } } } function calculateUnits(playerState, position) { var type = UNITS_TYPES[position]; return TRAINED_COLOR + playerState.statistics.unitsTrained[type] + '[/color] / ' + LOST_COLOR + playerState.statistics.unitsLost[type] + '[/color] / ' + KILLED_COLOR + playerState.statistics.enemyUnitsKilled[type] + '[/color]'; } function calculateResources(playerState, position) { var type = RESOURCES_TYPES[position]; return INCOME_COLOR + playerState.statistics.resourcesGathered[type] + '[/color] / ' + OUTCOME_COLOR + (playerState.statistics.resourcesUsed[type] - playerState.statistics.resourcesSold[type]) + '[/color]'; } function calculateTotalResources(playerState, position) { var totalGathered = 0; var totalUsed = 0; for each (var type in RESOURCES_TYPES) { totalGathered += playerState.statistics.resourcesGathered[type]; totalUsed += playerState.statistics.resourcesUsed[type] - playerState.statistics.resourcesSold[type]; } return INCOME_COLOR + totalGathered + '[/color] / ' + OUTCOME_COLOR + totalUsed + '[/color]'; } function calculateTreasureCollected(playerState, position) { return playerState.statistics.treasuresCollected; } function calculateLootCollected(playerState, position) { return playerState.statistics.lootCollected; } function calculateTributeSent(playerState, position) { return INCOME_COLOR + playerState.statistics.tributesSent + "[/color] / " + OUTCOME_COLOR + playerState.statistics.tributesReceived + "[/color]"; } function calculateResourcesTeam(counters) { for (var t in g_Teams) { if (t == -1) continue; for (var w in counters) { var teamTotal = "undefined"; var total = { i : 0, o : 0 }; for (var p = 0; p < g_Teams[t]; ++p) { var caption = Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + p + "][" + w + "]").caption; // clean [Color=""], [/Color] and white space for make the sum more easy caption = caption.replace(/\[([\w\' \\\"\/\=]*)\]|\s/g, ""); if (w == 5) total.i += (+caption); else { var splitCaption = caption.split("/"); total.i += (+splitCaption[0]); total.o += (+splitCaption[1]); } } if (w == 5) teamTotal = total.i; else teamTotal = INCOME_COLOR + total.i + "[/color] / " + OUTCOME_COLOR + total.o + "[/color]"; Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + w + "]").caption = teamTotal; } } } function calculateResourceExchanged(playerState, position) { var type = RESOURCES_TYPES[position]; return INCOME_COLOR + '+' + playerState.statistics.resourcesBought[type] + '[/color] ' + OUTCOME_COLOR + '-' + playerState.statistics.resourcesSold[type] + '[/color]'; } function calculateBatteryEfficiency(playerState, position) { var totalBought = 0; for each (var boughtAmount in playerState.statistics.resourcesBought) totalBought += boughtAmount; var totalSold = 0; for each (var soldAmount in playerState.statistics.resourcesSold) totalSold += soldAmount; return Math.floor(totalSold > 0 ? (totalBought / totalSold) * 100 : 0) + "%"; } function calculateTradeIncome(playerState, position) { return playerState.statistics.tradeIncome; } function calculateMarketTeam(counters) { for (var t in g_Teams) { if (t == -1) continue; for (var w in counters) { var teamTotal = "undefined"; var total = { i : 0, o : 0 }; for (var p = 0; p < g_Teams[t]; ++p) { var caption = Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + p + "][" + w + "]").caption; // clean [Color=""], [/Color], white space, + and % for make the sum more easy caption = caption.replace(/\[([\w\' \\\"\/\=]*)\]|\s|\+|\%/g, ""); if (w >= 4) total.i += (+caption); else { var splitCaption = caption.split("-"); total.i += (+splitCaption[0]); total.o += (+splitCaption[1]); } } if (w >= 4) teamTotal = total.i +(w == 4 ? "%" : ""); else teamTotal = INCOME_COLOR + '+' + total.i + '[/color] ' + OUTCOME_COLOR + '-' + total.o + '[/color]'; Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + w + "]").caption = teamTotal; } } } function calculateVegetarianRatio(playerState, position) { if (!teamMiscHelperData[playerState.team]) teamMiscHelperData[playerState.team] = []; if (!teamMiscHelperData[playerState.team][position]) teamMiscHelperData[playerState.team][position] = {"food": 0, "vegetarianFood": 0}; if (playerState.statistics.resourcesGathered.vegetarianFood && playerState.statistics.resourcesGathered.food) { teamMiscHelperData[playerState.team][position].food += playerState.statistics.resourcesGathered.food; teamMiscHelperData[playerState.team][position].vegetarianFood += playerState.statistics.resourcesGathered.vegetarianFood; return Math.floor((playerState.statistics.resourcesGathered.vegetarianFood / playerState.statistics.resourcesGathered.food) * 100) + "%"; } else return 0 + "%"; } function calculateFeminisation(playerState, position) { if (!teamMiscHelperData[playerState.team]) teamMiscHelperData[playerState.team] = []; if (!teamMiscHelperData[playerState.team][position]) teamMiscHelperData[playerState.team][position] = {"Female": 0, "Worker": 0}; if (playerState.statistics.unitsTrained.Worker && playerState.statistics.unitsTrained.Female) { teamMiscHelperData[playerState.team][position].Female = playerState.statistics.unitsTrained.Female; teamMiscHelperData[playerState.team][position].Worker = playerState.statistics.unitsTrained.Worker; return Math.floor((playerState.statistics.unitsTrained.Female / playerState.statistics.unitsTrained.Worker) * 100) + "%"; } else return 0 + "%"; } function calculateKillDeathRatio(playerState, position) { if (!teamMiscHelperData[playerState.team]) teamMiscHelperData[playerState.team] = []; if (!teamMiscHelperData[playerState.team][position]) teamMiscHelperData[playerState.team][position] = {"enemyUnitsKilled": 0, "unitsLost": 0}; teamMiscHelperData[playerState.team][position].enemyUnitsKilled = playerState.statistics.enemyUnitsKilled.total; teamMiscHelperData[playerState.team][position].unitsLost = playerState.statistics.unitsLost.total; if (!playerState.statistics.enemyUnitsKilled.total) return DEFAULT_DECIMAL; if (!playerState.statistics.unitsLost.total) // and enemyUnitsKilled.total > 0 return INFINITE_SYMBOL; // infinity symbol return Math.round((playerState.statistics.enemyUnitsKilled.total / playerState.statistics.unitsLost.total)*100)/100; } function calculateMapExploration(playerState, position) { if (!teamMiscHelperData[playerState.team]) teamMiscHelperData[playerState.team] = []; teamMiscHelperData[playerState.team][position] = playerState.statistics.teamPercentMapExplored; return playerState.statistics.percentMapExplored + "%"; } +function calculateMapFinalControl(playerState, position) +{ + if (!teamMiscHelperData[playerState.team]) + teamMiscHelperData[playerState.team] = []; + + teamMiscHelperData[playerState.team][position] = playerState.statistics.teamPercentMapControlled; + return playerState.statistics.percentMapControlled + "%"; +} + +function calculateMapPeakControl(playerState, position) +{ + if (!teamMiscHelperData[playerState.team]) + teamMiscHelperData[playerState.team] = []; + + teamMiscHelperData[playerState.team][position] = playerState.statistics.teamPeakPercentMapControlled; + return playerState.statistics.peakPercentMapControlled + "%"; +} + function calculateMiscellaneous(counters) { for (var t in g_Teams) { if (t == -1) continue; for (var w in counters) { var teamTotal = "undefined"; if (w == 0) teamTotal = (teamMiscHelperData[t][w].food == 0 ? "0" : Math.floor((teamMiscHelperData[t][w].vegetarianFood / teamMiscHelperData[t][w].food) * 100)) + "%"; else if (w == 1) teamTotal = (teamMiscHelperData[t][w].Worker == 0 ? "0" : Math.floor((teamMiscHelperData[t][w].Female / teamMiscHelperData[t][w].Worker) * 100)) + "%"; else if (w == 2) { if (!teamMiscHelperData[t][w].enemyUnitsKilled) teamTotal = DEFAULT_DECIMAL; else if (!teamMiscHelperData[t][w].unitsLost) // and enemyUnitsKilled.total > 0 teamTotal = INFINITE_SYMBOL; // infinity symbol else teamTotal = Math.round((teamMiscHelperData[t][w].enemyUnitsKilled / teamMiscHelperData[t][w].unitsLost)*100)/100; } - else if (w == 3) + else if (w >= 3) teamTotal = teamMiscHelperData[t][w] + "%"; Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + w + "]").caption = teamTotal; } } } Index: ps/trunk/binaries/data/mods/public/gui/summary/layout.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/summary/layout.js (revision 16932) +++ ps/trunk/binaries/data/mods/public/gui/summary/layout.js (revision 16933) @@ -1,288 +1,300 @@ var panelsData = [ { // Scores panel "headings": [ // headings on score panel { "caption": translate("Player name"), "yStart": 26, "width": 200 }, { "caption": translate("Economy score"), "yStart": 16, "width": 100 }, { "caption": translate("Military score"), "yStart": 16, "width": 100 }, { "caption": translate("Exploration score"), "yStart": 16, "width": 100 }, { "caption": translate("Total score"), "yStart": 16, "width": 100 } ], "titleHeadings": [], "counters": [ // counters on score panel { "width": 100, "fn": calculateEconomyScore }, { "width": 100, "fn": calculateMilitaryScore }, { "width": 100, "fn": calculateExplorationScore }, { "width": 100, "fn": calculateScoreTotal} ], "teamCounterFn": calculateScoreTeam }, { // buildings panel "headings": [ // headings on buildings panel { "caption": translate("Player name"), "yStart": 26, "width": 200 }, { "caption": translate("Total"), "yStart": 34, "width": 105 }, { "caption": translate("Houses"), "yStart": 34, "width": 85 }, { "caption": translate("Economic"), "yStart": 34, "width": 85 }, { "caption": translate("Outposts"), "yStart": 34, "width": 85 }, { "caption": translate("Military"), "yStart": 34, "width": 85 }, { "caption": translate("Fortresses"), "yStart": 34, "width": 85 }, { "caption": translate("Civ centers"), "yStart": 34, "width": 85 }, { "caption": translate("Wonders"), "yStart": 34, "width": 85 } ], "titleHeadings": [ { "caption": translate("Buildings Statistics (Constructed / Lost / Destroyed)"), "yStart": 16, "width": (85 * 7 + 105) }, // width = 700 ], "counters": [ // counters on buildings panel {"width": 105, "fn": calculateBuildings}, {"width": 85, "fn": calculateBuildings}, {"width": 85, "fn": calculateBuildings}, {"width": 85, "fn": calculateBuildings}, {"width": 85, "fn": calculateBuildings}, {"width": 85, "fn": calculateBuildings}, {"width": 85, "fn": calculateBuildings}, {"width": 85, "fn": calculateBuildings} ], "teamCounterFn": calculateColorsTeam }, { // units panel "headings": [ // headings on units panel { "caption": translate("Player name"), "yStart": 26, "width": 200 }, { "caption": translate("Total"), "yStart": 34, "width": 120 }, { "caption": translate("Infantry"), "yStart": 34, "width": 100 }, { "caption": translate("Worker"), "yStart": 34, "width": 100 }, { "caption": translate("Cavalry"), "yStart": 34, "width": 100 }, { "caption": translate("Champion"), "yStart": 34, "width": 100 }, { "caption": translate("Heroes"), "yStart": 34, "width": 100 }, { "caption": translate("Navy"), "yStart": 34, "width": 100 }, { "caption": translate("Traders"), "yStart": 34, "width": 100 } ], "titleHeadings": [ { "caption": translate("Units Statistics (Trained / Lost / Killed)"), "yStart": 16, "width": (100 * 7 + 120) }, // width = 820 ], "counters": [ // counters on units panel {"width": 120, "fn": calculateUnits}, {"width": 100, "fn": calculateUnits}, {"width": 100, "fn": calculateUnits}, {"width": 100, "fn": calculateUnits}, {"width": 100, "fn": calculateUnits}, {"width": 100, "fn": calculateUnits}, {"width": 100, "fn": calculateUnits}, {"width": 100, "fn": calculateUnits} ], "teamCounterFn": calculateColorsTeam }, { // resources panel "headings": [ // headings on resources panel { "caption": translate("Player name"), "yStart": 26, "width": 200 }, { "caption": translate("Food"), "yStart": 34, "width": 100 }, { "caption": translate("Wood"), "yStart": 34, "width": 100 }, { "caption": translate("Stone"), "yStart": 34, "width": 100 }, { "caption": translate("Metal"), "yStart": 34, "width": 100 }, { "caption": translate("Total"), "yStart": 34, "width": 110 }, { "caption": translate("Treasures collected"), "yStart": 16, "width": 100 }, { "caption": translate("Tributes (Sent / Received)"), "yStart": 16, "width": 121 }, { "caption": translate("Loot"), "yStart": 16, "width": 100 } ], "titleHeadings": [ { "caption": translate("Resource Statistics (Gathered / Used)"), "yStart": 16, "width": (100 * 4 + 110) }, // width = 510 ], "counters": [ // counters on resources panel {"width": 100, "fn": calculateResources}, {"width": 100, "fn": calculateResources}, {"width": 100, "fn": calculateResources}, {"width": 100, "fn": calculateResources}, {"width": 110, "fn": calculateTotalResources}, {"width": 100, "fn": calculateTreasureCollected}, {"width": 121, "fn": calculateTributeSent}, {"width": 100, "fn": calculateLootCollected} ], "teamCounterFn": calculateResourcesTeam }, { // market panel "headings": [ // headings on market panel { "caption": translate("Player name"), "yStart": 26, "width": 200 }, { "caption": translate("Food exchanged"), "yStart": 16, "width": 100 }, { "caption": translate("Wood exchanged"), "yStart": 16, "width": 100 }, { "caption": translate("Stone exchanged"), "yStart": 16, "width": 100 }, { "caption": translate("Metal exchanged"), "yStart": 16, "width": 100 }, { "caption": translate("Barter efficiency"), "yStart": 16, "width": 100 }, { "caption": translate("Trade income"), "yStart": 16, "width": 100 } ], "titleHeadings": [], "counters": [ // counters on market panel {"width": 100, "fn": calculateResourceExchanged}, {"width": 100, "fn": calculateResourceExchanged}, {"width": 100, "fn": calculateResourceExchanged}, {"width": 100, "fn": calculateResourceExchanged}, {"width": 100, "fn": calculateBatteryEfficiency}, {"width": 100, "fn": calculateTradeIncome} ], "teamCounterFn": calculateMarketTeam }, { // miscellaneous panel "headings": [ // headings on miscellaneous panel { "caption": translate("Player name"), "yStart": 26, "width": 200 }, { "caption": translate("Vegetarian\nratio"), "yStart": 16, "width": 100 }, { "caption": translate("Feminisation"), "yStart": 16, "width": 100 }, { "caption": translate("Kill / Death\nratio"), "yStart": 16, "width": 100 }, - { "caption": translate("Map\nexploration"), "yStart": 16, "width": 100 } + { "caption": translate("Map\nexploration"), "yStart": 16, "width": 100 }, + { "caption": translate("At peak"), "yStart": 34, "width": 100 }, + { "caption": translate("At finish"), "yStart": 34, "width": 100 } + ], + "titleHeadings": [ + { "caption": translate("Map control"), "xOffset": 400, "yStart": 16, "width": 200 } ], - "titleHeadings": [], "counters": [ // counters on miscellaneous panel {"width": 100, "fn": calculateVegetarianRatio}, {"width": 100, "fn": calculateFeminisation}, {"width": 100, "fn": calculateKillDeathRatio}, - {"width": 100, "fn": calculateMapExploration} + {"width": 100, "fn": calculateMapExploration}, + {"width": 100, "fn": calculateMapPeakControl}, + {"width": 100, "fn": calculateMapFinalControl} ], "teamCounterFn": calculateMiscellaneous } ]; function resetGeneralPanel() { for (var h = 0; h < MAX_HEADINGTITLE; ++h) { Engine.GetGUIObjectByName("titleHeading["+ h +"]").hidden = true; Engine.GetGUIObjectByName("Heading[" + h + "]").hidden = true; for (var p = 0; p < MAX_SLOTS; ++p) { Engine.GetGUIObjectByName("valueData[" + p + "][" + h + "]").hidden = true; for (var t = 0; t < MAX_TEAMS; ++t) { Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + p + "][" + h + "]").hidden = true; Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + h + "]").hidden = true; } } } } function updateGeneralPanelHeadings(headings) { var left = 50; for (var h in headings) { var headerGUIName = "playerNameHeading"; if (h > 0) headerGUIName = "Heading[" + (h - 1) + "]"; var headerGUI = Engine.GetGUIObjectByName(headerGUIName); headerGUI.caption = headings[h].caption; headerGUI.size = left + " " + headings[h].yStart + " " + (left + headings[h].width) + " 100%"; headerGUI.hidden = false; if (headings[h].width < LONG_HEADING_WIDTH) left += headings[h].width; } } function updateGeneralPanelTitles(titleHeadings) { var left = 250; for (var th in titleHeadings) { if (th >= MAX_HEADINGTITLE) break; + if (titleHeadings[th].xOffset) + left += titleHeadings[th].xOffset; + var headerGUI = Engine.GetGUIObjectByName("titleHeading["+ th +"]"); headerGUI.caption = titleHeadings[th].caption; headerGUI.size = left + " " + titleHeadings[th].yStart + " " + (left + titleHeadings[th].width) + " 100%"; headerGUI.hidden = false; + + if (titleHeadings[th].width < LONG_HEADING_WIDTH) + left += titleHeadings[th].width; } } function updateGeneralPanelCounter(counters) { var rowPlayerObjectWidth = 0; var left = 0; for (var p = 0; p < MAX_SLOTS; ++p) { left = 240; var counterObject; for (var w in counters) { counterObject = Engine.GetGUIObjectByName("valueData[" + p + "][" + w + "]"); counterObject.size = left + " 6 " + (left + counters[w].width) + " 100%"; counterObject.hidden = false; left += counters[w].width; } if (rowPlayerObjectWidth == 0) rowPlayerObjectWidth = left; var counterTotalObject; for (var t = 0; t < MAX_TEAMS; ++t) { left = 240; for (var w in counters) { counterObject = Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + p + "][" + w + "]"); counterObject.size = left + " 6 " + (left + counters[w].width) + " 100%"; counterObject.hidden = false; if (g_Teams[t]) { var yStart = 30 + g_Teams[t] * (PLAYER_BOX_Y_SIZE + PLAYER_BOX_GAP) + 2; counterTotalObject = Engine.GetGUIObjectByName("valueDataTeam[" + t + "][" + w + "]"); counterTotalObject.size = (left + 20) + " " + yStart + " " + (left + counters[w].width) + " 100%"; counterTotalObject.hidden = false; } left += counters[w].width; } } } return rowPlayerObjectWidth; } function updateGeneralPanelTeams() { if (!g_Teams || g_WithoutTeam > 0) Engine.GetGUIObjectByName("noTeamsBox").hidden = false; if (!g_Teams) return; var yStart = TEAMS_BOX_Y_START + g_WithoutTeam * (PLAYER_BOX_Y_SIZE + PLAYER_BOX_GAP); for (var i = 0; i < g_Teams.length; ++i) { if (!g_Teams[i]) continue; var teamBox = Engine.GetGUIObjectByName("teamBoxt["+i+"]"); teamBox.hidden = false; var teamBoxSize = teamBox.size; teamBoxSize.top = yStart; teamBox.size = teamBoxSize; yStart += 30 + g_Teams[i] * (PLAYER_BOX_Y_SIZE + PLAYER_BOX_GAP) + 32; Engine.GetGUIObjectByName("teamNameHeadingt["+i+"]").caption = "Team "+(i+1); var teamHeading = Engine.GetGUIObjectByName("teamHeadingt["+i+"]"); var yStartTotal = 30 + g_Teams[i] * (PLAYER_BOX_Y_SIZE + PLAYER_BOX_GAP) + 2; teamHeading.size = "50 "+yStartTotal+" 100% "+(yStartTotal+20); teamHeading.caption = translate("Team total"); } // If there are no players without team, hide "player name" heading if (!g_WithoutTeam) Engine.GetGUIObjectByName("playerNameHeading").caption = ""; } function updateObjectPlayerPosition() { for (var h = 0; h < MAX_SLOTS; ++h) { var playerBox = Engine.GetGUIObjectByName("playerBox[" + h + "]"); var boxSize = playerBox.size; boxSize.top += h * (PLAYER_BOX_Y_SIZE + PLAYER_BOX_GAP); boxSize.bottom = boxSize.top + PLAYER_BOX_Y_SIZE; playerBox.size = boxSize; for (var i = 0; i < MAX_TEAMS; ++i) { var playerBoxt = Engine.GetGUIObjectByName("playerBoxt[" + i + "][" + h + "]"); boxSize = playerBoxt.size; boxSize.top += h * (PLAYER_BOX_Y_SIZE + PLAYER_BOX_GAP); boxSize.bottom = boxSize.top + PLAYER_BOX_Y_SIZE; playerBoxt.size = boxSize; }; }; } Index: ps/trunk/binaries/data/mods/public/simulation/components/StatisticsTracker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/StatisticsTracker.js (revision 16932) +++ ps/trunk/binaries/data/mods/public/simulation/components/StatisticsTracker.js (revision 16933) @@ -1,397 +1,447 @@ function StatisticsTracker() {} StatisticsTracker.prototype.Schema = ""; StatisticsTracker.prototype.Init = function() { // units this.unitsClasses = [ "Infantry", "Worker", "Female", "Cavalry", "Champion", "Hero", "Ship", "Trader" ]; this.unitsTrained = { "Infantry": 0, "Worker": 0, "Female": 0, "Cavalry": 0, "Champion": 0, "Hero": 0, "Ship": 0, "Trader": 0, "total": 0 }; this.unitsLost = { "Infantry": 0, "Worker": 0, "Female": 0, "Cavalry": 0, "Champion": 0, "Hero": 0, "Ship": 0, "Trader": 0, "total": 0 }; this.unitsLostValue = 0; this.enemyUnitsKilled = { "Infantry": 0, "Worker": 0, "Female": 0, "Cavalry": 0, "Champion": 0, "Hero": 0, "Ship": 0, "Trader": 0, "total": 0 }; this.enemyUnitsKilledValue = 0; // buildings this.buildingsClasses = [ "House", "Economic", "Outpost", "Military", "Fortress", "CivCentre", "Wonder" ]; this.buildingsConstructed = { "House": 0, "Economic": 0, "Outpost": 0, "Military": 0, "Fortress": 0, "CivCentre": 0, "Wonder": 0, "total": 0 }; this.buildingsLost = { "House": 0, "Economic": 0, "Outpost": 0, "Military": 0, "Fortress": 0, "CivCentre": 0, "Wonder": 0, "total": 0 }; this.buildingsLostValue = 0; this.enemyBuildingsDestroyed = { "House": 0, "Economic": 0, "Outpost": 0, "Military": 0, "Fortress": 0, "CivCentre": 0, "Wonder": 0, "total": 0 }; this.enemyBuildingsDestroyedValue = 0; // resources this.resourcesGathered = { "food": 0, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }; this.resourcesUsed = { "food": 0, "wood": 0, "metal": 0, "stone": 0 }; this.resourcesSold = { "food": 0, "wood": 0, "metal": 0, "stone": 0 }; this.resourcesBought = { "food": 0, "wood": 0, "metal": 0, "stone": 0 }; this.tributesSent = 0; this.tributesReceived = 0; this.tradeIncome = 0; this.treasuresCollected = 0; - this.lootCollected = 0; + this.lootCollected = 0; + this.peakPercentMapControlled = 0; + this.teamPeakPercentMapControlled = 0; }; /** * Returns a subset of statistics that will be added to the simulation state, * thus called each turn. Basic statistics should not contain data that would * be expensive to compute. * * Note: as of now, nothing in the game needs that, but some AIs developed by * modders need it in the API. */ StatisticsTracker.prototype.GetBasicStatistics = function() { return { "resourcesGathered": this.resourcesGathered, "percentMapExplored": this.GetPercentMapExplored() }; }; StatisticsTracker.prototype.GetStatistics = function() { return { "unitsTrained": this.unitsTrained, "unitsLost": this.unitsLost, "unitsLostValue": this.unitsLostValue, "enemyUnitsKilled": this.enemyUnitsKilled, "enemyUnitsKilledValue": this.enemyUnitsKilledValue, "buildingsConstructed": this.buildingsConstructed, "buildingsLost": this.buildingsLost, "buildingsLostValue": this.buildingsLostValue, "enemyBuildingsDestroyed": this.enemyBuildingsDestroyed, "enemyBuildingsDestroyedValue": this.enemyBuildingsDestroyedValue, "resourcesGathered": this.resourcesGathered, "resourcesUsed": this.resourcesUsed, "resourcesSold": this.resourcesSold, "resourcesBought": this.resourcesBought, "tributesSent": this.tributesSent, "tributesReceived": this.tributesReceived, "tradeIncome": this.tradeIncome, "treasuresCollected": this.treasuresCollected, "lootCollected": this.lootCollected, "percentMapExplored": this.GetPercentMapExplored(), - "teamPercentMapExplored": this.GetTeamPercentMapExplored() + "teamPercentMapExplored": this.GetTeamPercentMapExplored(), + "percentMapControlled": this.GetPercentMapControlled(), + "teamPercentMapControlled": this.GetTeamPercentMapControlled(), + "peakPercentMapControlled": this.peakPercentMapControlled, + "teamPeakPercentMapControlled": this.teamPeakPercentMapControlled }; }; /** * Increments counter associated with certain entity/counter and type of given entity. * @param cmpIdentity The entity identity component * @param counter The name of the counter to increment (e.g. "unitsTrained") * @param type The type of the counter (e.g. "workers") */ StatisticsTracker.prototype.CounterIncrement = function(cmpIdentity, counter, type) { var classes = cmpIdentity.GetClassesList(); if (!classes) return; if (classes.indexOf(type) != -1) this[counter][type]++; }; /** * Counts the total number of units trained as well as an individual count for * each unit type. Based on templates. * @param trainedUnit The unit that has been trained */ StatisticsTracker.prototype.IncreaseTrainedUnitsCounter = function(trainedUnit) { var cmpUnitEntityIdentity = Engine.QueryInterface(trainedUnit, IID_Identity); if (!cmpUnitEntityIdentity) return; for each (var type in this.unitsClasses) this.CounterIncrement(cmpUnitEntityIdentity, "unitsTrained", type); this.unitsTrained.total++; }; /** * Counts the total number of buildings constructed as well as an individual count for * each building type. Based on templates. * @param constructedBuilding The building that has been constructed */ StatisticsTracker.prototype.IncreaseConstructedBuildingsCounter = function(constructedBuilding) { var cmpBuildingEntityIdentity = Engine.QueryInterface(constructedBuilding, IID_Identity); if (!cmpBuildingEntityIdentity) return; for each(var type in this.buildingsClasses) this.CounterIncrement(cmpBuildingEntityIdentity, "buildingsConstructed", type); this.buildingsConstructed.total++; }; StatisticsTracker.prototype.KilledEntity = function(targetEntity) { var cmpTargetEntityIdentity = Engine.QueryInterface(targetEntity, IID_Identity); var cmpCost = Engine.QueryInterface(targetEntity, IID_Cost); var costs = cmpCost.GetResourceCosts(); if (!cmpTargetEntityIdentity) return; var cmpFoundation = Engine.QueryInterface(targetEntity, IID_Foundation); // We want to deal only with real structures, not foundations var targetIsStructure = cmpTargetEntityIdentity.HasClass("Structure") && cmpFoundation == null; var targetIsDomesticAnimal = cmpTargetEntityIdentity.HasClass("Animal") && cmpTargetEntityIdentity.HasClass("Domestic"); // Don't count domestic animals as units var targetIsUnit = cmpTargetEntityIdentity.HasClass("Unit") && !targetIsDomesticAnimal; var cmpTargetOwnership = Engine.QueryInterface(targetEntity, IID_Ownership); // Don't increase counters if target player is gaia (player 0) if (cmpTargetOwnership.GetOwner() == 0) return; if (targetIsUnit) { for each (var type in this.unitsClasses) this.CounterIncrement(cmpTargetEntityIdentity, "enemyUnitsKilled", type); this.enemyUnitsKilled.total++; for each (var cost in costs) this.enemyUnitsKilledValue += cost; } if (targetIsStructure) { for each (var type in this.buildingsClasses) this.CounterIncrement(cmpTargetEntityIdentity, "enemyBuildingsDestroyed", type); this.enemyBuildingsDestroyed.total++; for each (var cost in costs) this.enemyBuildingsDestroyedValue += cost; } }; StatisticsTracker.prototype.LostEntity = function(lostEntity) { var cmpLostEntityIdentity = Engine.QueryInterface(lostEntity, IID_Identity); var cmpCost = Engine.QueryInterface(lostEntity, IID_Cost); var costs = cmpCost.GetResourceCosts(); if (!cmpLostEntityIdentity) return; var cmpFoundation = Engine.QueryInterface(lostEntity, IID_Foundation); // We want to deal only with real structures, not foundations var lostEntityIsStructure = cmpLostEntityIdentity.HasClass("Structure") && cmpFoundation == null; var lostEntityIsDomesticAnimal = cmpLostEntityIdentity.HasClass("Animal") && cmpLostEntityIdentity.HasClass("Domestic"); // Don't count domestic animals as units var lostEntityIsUnit = cmpLostEntityIdentity.HasClass("Unit") && !lostEntityIsDomesticAnimal; if (lostEntityIsUnit) { for each (var type in this.unitsClasses) this.CounterIncrement(cmpLostEntityIdentity, "unitsLost", type); this.unitsLost.total++; for each (var cost in costs) this.unitsLostValue += cost; } if (lostEntityIsStructure) { for each (var type in this.buildingsClasses) this.CounterIncrement(cmpLostEntityIdentity, "buildingsLost", type); this.buildingsLost.total++; for each (var cost in costs) this.buildingsLostValue += cost; } }; /** * @param type Generic type of resource (string) * @param amount Amount of resource, whick should be added (integer) * @param specificType Specific type of resource (string, optional) */ StatisticsTracker.prototype.IncreaseResourceGatheredCounter = function(type, amount, specificType) { this.resourcesGathered[type] += amount; if (type == "food" && (specificType == "fruit" || specificType == "grain")) this.resourcesGathered.vegetarianFood += amount; }; /** * @param type Generic type of resource (string) * @param amount Amount of resource, which should be added (integer) */ StatisticsTracker.prototype.IncreaseResourceUsedCounter = function(type, amount) { this.resourcesUsed[type] += amount; }; StatisticsTracker.prototype.IncreaseTreasuresCollectedCounter = function() { this.treasuresCollected++; }; StatisticsTracker.prototype.IncreaseLootCollectedCounter = function(amount) { for (let type in amount) this.lootCollected += amount[type]; }; StatisticsTracker.prototype.IncreaseResourcesSoldCounter = function(type, amount) { this.resourcesSold[type] += amount; }; StatisticsTracker.prototype.IncreaseResourcesBoughtCounter = function(type, amount) { this.resourcesBought[type] += amount; }; StatisticsTracker.prototype.IncreaseTributesSentCounter = function(amount) { this.tributesSent += amount; }; StatisticsTracker.prototype.IncreaseTributesReceivedCounter = function(amount) { this.tributesReceived += amount; }; StatisticsTracker.prototype.IncreaseTradeIncomeCounter = function(amount) { this.tradeIncome += amount; }; StatisticsTracker.prototype.GetPercentMapExplored = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); return cmpRangeManager.GetPercentMapExplored(cmpPlayer.GetPlayerID()); }; /** * Note: cmpRangeManager.GetUnionPercentMapExplored computes statistics from scratch! * As a consequence, this function should not be called too often. */ StatisticsTracker.prototype.GetTeamPercentMapExplored = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer) return 0; var team = cmpPlayer.GetTeam(); // If teams are not locked, this statistic won't be displayed, so don't bother computing if (team == -1 || !cmpPlayer.GetLockTeams()) return cmpRangeManager.GetPercentMapExplored(cmpPlayer.GetPlayerID()); var teamPlayers = []; for (var i = 1; i < cmpPlayerManager.GetNumPlayers(); ++i) { let cmpOtherPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(i), IID_Player); if (cmpOtherPlayer && cmpOtherPlayer.GetTeam() == team) teamPlayers.push(i); } return cmpRangeManager.GetUnionPercentMapExplored(teamPlayers); -}; - +}; + +StatisticsTracker.prototype.GetPercentMapControlled = function() +{ + var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); + if (!cmpPlayer || !cmpTerritoryManager) + return 0; + + return cmpTerritoryManager.GetTerritoryPercentage(cmpPlayer.GetPlayerID()); +}; + +StatisticsTracker.prototype.GetTeamPercentMapControlled = function() +{ + var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); + if (!cmpPlayer || !cmpTerritoryManager) + return 0; + + var team = cmpPlayer.GetTeam(); + if (team == -1 || !cmpPlayer.GetLockTeams()) + return cmpTerritoryManager.GetTerritoryPercentage(cmpPlayer.GetPlayerID()); + + var teamPercent = 0; + for (let i = 1; i < cmpPlayerManager.GetNumPlayers(); ++i) + { + let cmpOtherPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(i), IID_Player); + if (cmpOtherPlayer && cmpOtherPlayer.GetTeam() == team) + teamPercent += cmpTerritoryManager.GetTerritoryPercentage(i); + } + + return teamPercent; +}; + +StatisticsTracker.prototype.OnTerritoriesChanged = function(msg) +{ + var newPercent = this.GetPercentMapControlled(); + if (newPercent > this.peakPercentMapControlled) + this.peakPercentMapControlled = newPercent; + + newPercent = this.GetTeamPercentMapControlled(); + if (newPercent > this.teamPeakPercentMapControlled) + this.teamPeakPercentMapControlled = newPercent; +}; + Engine.RegisterComponentType(IID_StatisticsTracker, "StatisticsTracker", StatisticsTracker); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 16932) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 16933) @@ -1,497 +1,513 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/AlertRaiser.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Barter.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/CeasefireManager.js"); Engine.LoadComponentScript("interfaces/DamageReceiver.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Gate.js"); Engine.LoadComponentScript("interfaces/Guard.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/RallyPoint.js"); Engine.LoadComponentScript("interfaces/ResourceDropsite.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js") Engine.LoadComponentScript("interfaces/Trader.js") Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("GuiInterface.js"); var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface"); AddMock(SYSTEM_ENTITY, IID_Barter, { GetPrices: function() { return { "buy": { "food": 150 }, "sell": { "food": 25 }, }}, }); AddMock(SYSTEM_ENTITY, IID_EndGameManager, { GetGameType: function() { return "conquest"; } }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { GetNumPlayers: function() { return 2; }, GetPlayerByID: function(id) { TS_ASSERT(id === 0 || id === 1); return 100+id; }, }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { GetLosVisibility: function(ent, player) { return "visible"; }, GetLosCircular: function() { return false; }, }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { GetCurrentTemplateName: function(ent) { return "example"; }, GetTemplate: function(name) { return ""; }, }); AddMock(SYSTEM_ENTITY, IID_Timer, { GetTime: function() { return 0; }, SetTimeout: function(ent, iid, funcname, time, data) { return 0; }, }); AddMock(100, IID_Player, { GetName: function() { return "Player 1"; }, GetCiv: function() { return "gaia"; }, GetColor: function() { return { r: 1, g: 1, b: 1, a: 1}; }, GetPopulationCount: function() { return 10; }, GetPopulationLimit: function() { return 20; }, GetMaxPopulation: function() { return 200; }, GetResourceCounts: function() { return { food: 100 }; }, GetHeroes: function() { return []; }, IsTrainingBlocked: function() { return false; }, GetState: function() { return "active"; }, GetTeam: function() { return -1; }, GetLockTeams: function() { return false; }, GetCheatsEnabled: function() { return false; }, GetDiplomacy: function() { return [-1, 1]; }, IsAlly: function() { return false; }, IsMutualAlly: function() { return false; }, IsNeutral: function() { return false; }, IsEnemy: function() { return true; }, GetDisabledTemplates: function() { return {}; }, }); AddMock(100, IID_EntityLimits, { GetLimits: function() { return {"Foo": 10}; }, GetCounts: function() { return {"Foo": 5}; }, GetLimitChangers: function() {return {"Foo": {}}; } }); AddMock(100, IID_TechnologyManager, { IsTechnologyResearched: function(tech) { if (tech == "phase_village") return true; else return false; }, GetQueuedResearch: function() { return {}; }, GetStartedResearch: function() { return {}; }, GetResearchedTechs: function() { return {}; }, GetClassCounts: function() { return {}; }, GetTypeCountsByClass: function() { return {}; }, GetTechModifications: function() { return {}; }, }); AddMock(100, IID_StatisticsTracker, { GetBasicStatistics: function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0, }, "percentMapExplored": 10 }; }, GetStatistics: function() { return { "unitsTrained": 10, "unitsLost": 9, "buildingsConstructed": 5, "buildingsLost": 4, "civCentresBuilt": 1, "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0, }, "treasuresCollected": 0, "lootCollected": 0, "percentMapExplored": 10, - "teamPercentMapExplored": 10 + "teamPercentMapExplored": 10, + "percentMapControlled": 10, + "teamPercentMapControlled": 10, + "peakPercentOfMapControlled": 10, + "teamPeakPercentOfMapControlled": 10 }; }, IncreaseTrainedUnitsCounter: function() { return 1; }, IncreaseConstructedBuildingsCounter: function() { return 1; }, IncreaseBuiltCivCentresCounter: function() { return 1; }, }); AddMock(101, IID_Player, { GetName: function() { return "Player 2"; }, GetCiv: function() { return "mace"; }, GetColor: function() { return { r: 1, g: 0, b: 0, a: 1}; }, GetPopulationCount: function() { return 40; }, GetPopulationLimit: function() { return 30; }, GetMaxPopulation: function() { return 300; }, GetResourceCounts: function() { return { food: 200 }; }, GetHeroes: function() { return []; }, IsTrainingBlocked: function() { return false; }, GetState: function() { return "active"; }, GetTeam: function() { return -1; }, GetLockTeams: function() {return false; }, GetCheatsEnabled: function() { return false; }, GetDiplomacy: function() { return [-1, 1]; }, IsAlly: function() { return true; }, IsMutualAlly: function() {return false; }, IsNeutral: function() { return false; }, IsEnemy: function() { return false; }, GetDisabledTemplates: function() { return {}; }, }); AddMock(101, IID_EntityLimits, { GetLimits: function() { return {"Bar": 20}; }, GetCounts: function() { return {"Bar": 0}; }, GetLimitChangers: function() {return {"Bar": {}}; } }); AddMock(101, IID_TechnologyManager, { IsTechnologyResearched: function(tech) { if (tech == "phase_village") return true; else return false; }, GetQueuedResearch: function() { return {}; }, GetStartedResearch: function() { return {}; }, GetResearchedTechs: function() { return {}; }, GetClassCounts: function() { return {}; }, GetTypeCountsByClass: function() { return {}; }, GetTechModifications: function() { return {}; }, }); AddMock(101, IID_StatisticsTracker, { GetBasicStatistics: function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0, }, "percentMapExplored": 10 }; }, GetStatistics: function() { return { "unitsTrained": 10, "unitsLost": 9, "buildingsConstructed": 5, "buildingsLost": 4, "civCentresBuilt": 1, "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0, }, "treasuresCollected": 0, "lootCollected": 0, "percentMapExplored": 10, - "teamPercentMapExplored": 10 + "teamPercentMapExplored": 10, + "percentMapControlled": 10, + "teamPercentMapControlled": 10, + "peakPercentOfMapControlled": 10, + "teamPeakPercentOfMapControlled": 10 }; }, IncreaseTrainedUnitsCounter: function() { return 1; }, IncreaseConstructedBuildingsCounter: function() { return 1; }, IncreaseBuiltCivCentresCounter: function() { return 1; }, }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { players: [ { name: "Player 1", civ: "gaia", color: { r:1, g:1, b:1, a:1 }, popCount: 10, popLimit: 20, popMax: 200, heroes: [], resourceCounts: { food: 100 }, trainingBlocked: false, state: "active", team: -1, teamsLocked: false, cheatsEnabled: false, disabledTemplates: {}, phase: "village", isAlly: [false, false], isMutualAlly: [false, false], isNeutral: [false, false], isEnemy: [true, true], entityLimits: {"Foo": 10}, entityCounts: {"Foo": 5}, entityLimitChangers: {"Foo": {}}, researchQueued: {}, researchStarted: {}, researchedTechs: {}, classCounts: {}, typeCountsByClass: {}, statistics: { resourcesGathered: { food: 100, wood: 0, metal: 0, stone: 0, vegetarianFood: 0, }, percentMapExplored: 10 }, }, { name: "Player 2", civ: "mace", color: { r:1, g:0, b:0, a:1 }, popCount: 40, popLimit: 30, popMax: 300, heroes: [], resourceCounts: { food: 200 }, trainingBlocked: false, state: "active", team: -1, teamsLocked: false, cheatsEnabled: false, disabledTemplates: {}, phase: "village", isAlly: [true, true], isMutualAlly: [false, false], isNeutral: [false, false], isEnemy: [false, false], entityLimits: {"Bar": 20}, entityCounts: {"Bar": 0}, entityLimitChangers: {"Bar": {}}, researchQueued: {}, researchStarted: {}, researchedTechs: {}, classCounts: {}, typeCountsByClass: {}, statistics: { resourcesGathered: { food: 100, wood: 0, metal: 0, stone: 0, vegetarianFood: 0, }, percentMapExplored: 10 }, } ], circularMap: false, timeElapsed: 0, gameType: "conquest", barterPrices: {buy: {food: 150}, sell: {food: 25}} }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), { players: [ { name: "Player 1", civ: "gaia", color: { r:1, g:1, b:1, a:1 }, popCount: 10, popLimit: 20, popMax: 200, heroes: [], resourceCounts: { food: 100 }, trainingBlocked: false, state: "active", team: -1, teamsLocked: false, cheatsEnabled: false, disabledTemplates: {}, phase: "village", isAlly: [false, false], isMutualAlly: [false, false], isNeutral: [false, false], isEnemy: [true, true], entityLimits: {"Foo": 10}, entityCounts: {"Foo": 5}, entityLimitChangers: {"Foo": {}}, researchQueued: {}, researchStarted: {}, researchedTechs: {}, classCounts: {}, typeCountsByClass: {}, statistics: { unitsTrained: 10, unitsLost: 9, buildingsConstructed: 5, buildingsLost: 4, civCentresBuilt: 1, resourcesGathered: { food: 100, wood: 0, metal: 0, stone: 0, vegetarianFood: 0, }, treasuresCollected: 0, lootCollected: 0, percentMapExplored: 10, - teamPercentMapExplored: 10 + teamPercentMapExplored: 10, + percentMapControlled: 10, + teamPercentMapControlled: 10, + peakPercentOfMapControlled: 10, + teamPeakPercentOfMapControlled: 10 }, }, { name: "Player 2", civ: "mace", color: { r:1, g:0, b:0, a:1 }, popCount: 40, popLimit: 30, popMax: 300, heroes: [], resourceCounts: { food: 200 }, trainingBlocked: false, state: "active", team: -1, teamsLocked: false, cheatsEnabled: false, disabledTemplates: {}, phase: "village", isAlly: [true, true], isMutualAlly: [false, false], isNeutral: [false, false], isEnemy: [false, false], entityLimits: {"Bar": 20}, entityCounts: {"Bar": 0}, entityLimitChangers: {"Bar": {}}, researchQueued: {}, researchStarted: {}, researchedTechs: {}, classCounts: {}, typeCountsByClass: {}, statistics: { unitsTrained: 10, unitsLost: 9, buildingsConstructed: 5, buildingsLost: 4, civCentresBuilt: 1, resourcesGathered: { food: 100, wood: 0, metal: 0, stone: 0, vegetarianFood: 0, }, treasuresCollected: 0, lootCollected: 0, percentMapExplored: 10, - teamPercentMapExplored: 10 + teamPercentMapExplored: 10, + percentMapControlled: 10, + teamPercentMapControlled: 10, + peakPercentOfMapControlled: 10, + teamPeakPercentOfMapControlled: 10 }, } ], circularMap: false, timeElapsed: 0, gameType: "conquest", barterPrices: {buy: {food: 150}, sell: {food: 25}} }); AddMock(10, IID_Builder, { GetEntitiesList: function() { return ["test1", "test2"]; }, }); AddMock(10, IID_Health, { GetHitpoints: function() { return 50; }, GetMaxHitpoints: function() { return 60; }, IsRepairable: function() { return false; }, IsUnhealable: function() { return false; }, }); AddMock(10, IID_Identity, { GetClassesList: function() { return ["class1", "class2"]; }, GetVisibleClassesList: function() { return ["class3", "class4"]; }, GetRank: function() { return "foo"; }, GetSelectionGroupName: function() { return "Selection Group Name"; }, HasClass: function() { return true; }, }); AddMock(10, IID_Position, { GetTurretParent: function() {return INVALID_ENTITY;}, GetPosition: function() { return {x:1, y:2, z:3}; }, GetRotation: function() { return {x:4, y:5, z:6}; }, IsInWorld: function() { return true; }, }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), { id: 10, template: "example", alertRaiser: null, builder: true, identity: { rank: "foo", classes: ["class1", "class2"], visibleClasses: ["class3", "class4"], selectionGroupName: "Selection Group Name", }, fogging: null, foundation: null, garrisonHolder: null, gate: null, guard: null, mirage: null, pack: null, player: -1, position: {x:1, y:2, z:3}, production: null, rallyPoint: null, resourceCarrying: null, rotation: {x:4, y:5, z:6}, trader: null, unitAI: null, visibility: "visible", hitpoints: 50, maxHitpoints: 60, needsRepair: false, needsHeal: true, }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedEntityState(-1, 10), { armour: null, attack: null, barterMarket: { prices: { "buy": {"food":150}, "sell": {"food":25} }, }, buildingAI: null, healer: null, obstruction: null, turretParent: null, promotion: null, resourceDropsite: null, resourceGatherRates: null, resourceSupply: null, }); Index: ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 16932) +++ ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 16933) @@ -1,727 +1,758 @@ /* Copyright (C) 2015 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 "simulation2/system/Component.h" #include "ICmpTerritoryManager.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "graphics/TerritoryBoundary.h" #include "maths/MathUtil.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "renderer/Scene.h" #include "renderer/TerrainOverlay.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpTerritoryInfluence.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Grid.h" #include "simulation2/helpers/Render.h" #include class CCmpTerritoryManager; class TerritoryOverlay : public TerrainTextureOverlay { NONCOPYABLE(TerritoryOverlay); public: CCmpTerritoryManager& m_TerritoryManager; TerritoryOverlay(CCmpTerritoryManager& manager); virtual void BuildTextureRGBA(u8* data, size_t w, size_t h); }; class CCmpTerritoryManager : public ICmpTerritoryManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeGloballyToMessageType(MT_ValueModification); componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_WaterChanged); componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_Interpolate); componentManager.SubscribeToMessageType(MT_RenderSubmit); } DEFAULT_COMPONENT_ALLOCATOR(TerritoryManager) static std::string GetSchema() { return ""; } u8 m_ImpassableCost; float m_BorderThickness; float m_BorderSeparation; // Player ID in bits 0-4 (TERRITORY_PLAYER_MASK) // connected flag in bit 4 (TERRITORY_CONNECTED_MASK) // blinking flag in bit 5 (TERRITORY_BLINKING_MASK) // processed flag in bit 7 (TERRITORY_PROCESSED_MASK) Grid* m_Territories; + std::vector m_TerritoryCellCounts; + u16 m_TerritoryTotalPassableCellCount; + // Saves the cost per tile (to stop territory on impassable tiles) Grid* m_CostGrid; // Set to true when territories change; will send a TerritoriesChanged message // during the Update phase bool m_TriggerEvent; struct SBoundaryLine { bool blinking; CColor color; SOverlayTexturedLine overlay; }; std::vector m_BoundaryLines; bool m_BoundaryLinesDirty; double m_AnimTime; // time since start of rendering, in seconds TerritoryOverlay* m_DebugOverlay; bool m_EnableLineDebugOverlays; ///< Enable node debugging overlays for boundary lines? std::vector m_DebugBoundaryLineNodes; virtual void Init(const CParamNode& UNUSED(paramNode)) { m_Territories = NULL; m_CostGrid = NULL; m_DebugOverlay = NULL; // m_DebugOverlay = new TerritoryOverlay(*this); m_BoundaryLinesDirty = true; m_TriggerEvent = true; m_EnableLineDebugOverlays = false; m_DirtyID = 1; m_AnimTime = 0.0; + m_TerritoryTotalPassableCellCount = 0; + // Register Relax NG validator CXeromyces::AddValidator(g_VFS, "territorymanager", "simulation/data/territorymanager.rng"); CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/territorymanager.xml", "territorymanager"); int impassableCost = externalParamNode.GetChild("TerritoryManager").GetChild("ImpassableCost").ToInt(); ENSURE(0 <= impassableCost && impassableCost <= 255); m_ImpassableCost = (u8)impassableCost; m_BorderThickness = externalParamNode.GetChild("TerritoryManager").GetChild("BorderThickness").ToFixed().ToFloat(); m_BorderSeparation = externalParamNode.GetChild("TerritoryManager").GetChild("BorderSeparation").ToFixed().ToFloat(); } virtual void Deinit() { SAFE_DELETE(m_Territories); SAFE_DELETE(m_CostGrid); SAFE_DELETE(m_DebugOverlay); } virtual void Serialize(ISerializer& UNUSED(serialize)) { // Territory state can be recomputed as required, so we don't need to serialize any of it. // TODO: do we ever need to serialize m_TriggerEvent to prevent lost messages? } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); MakeDirtyIfRelevantEntity(msgData.entity); break; } case MT_PositionChanged: { const CMessagePositionChanged& msgData = static_cast (msg); MakeDirtyIfRelevantEntity(msgData.entity); break; } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); if (msgData.component == L"TerritoryInfluence") MakeDirty(); break; } case MT_TerrainChanged: case MT_WaterChanged: { // also recalculate the cost grid to support atlas changes SAFE_DELETE(m_CostGrid); MakeDirty(); break; } case MT_Update: { if (m_TriggerEvent) { m_TriggerEvent = false; CMessageTerritoriesChanged msg; GetSimContext().GetComponentManager().BroadcastMessage(msg); } break; } case MT_Interpolate: { const CMessageInterpolate& msgData = static_cast (msg); Interpolate(msgData.deltaSimTime, msgData.offset); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } } } // Check whether the entity is either a settlement or territory influence; // ignore any others void MakeDirtyIfRelevantEntity(entity_id_t ent) { CmpPtr cmpTerritoryInfluence(GetSimContext(), ent); if (cmpTerritoryInfluence) MakeDirty(); } virtual const Grid& GetTerritoryGrid() { CalculateTerritories(); ENSURE(m_Territories); return *m_Territories; } virtual player_id_t GetOwner(entity_pos_t x, entity_pos_t z); virtual std::vector GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected); virtual bool IsConnected(entity_pos_t x, entity_pos_t z); virtual void SetTerritoryBlinking(entity_pos_t x, entity_pos_t z); // To support lazy updates of territory render data, // we maintain a DirtyID here and increment it whenever territories change; // if a caller has a lower DirtyID then it needs to be updated. size_t m_DirtyID; void MakeDirty() { SAFE_DELETE(m_Territories); ++m_DirtyID; m_BoundaryLinesDirty = true; m_TriggerEvent = true; } virtual bool NeedUpdate(size_t* dirtyID) { if (*dirtyID != m_DirtyID) { *dirtyID = m_DirtyID; return true; } return false; } void CalculateCostGrid(); void CalculateTerritories(); + u8 GetTerritoryPercentage(player_id_t player); + std::vector ComputeBoundaries(); void UpdateBoundaryLines(); void Interpolate(float frameTime, float frameOffset); void RenderSubmit(SceneCollector& collector); }; REGISTER_COMPONENT_TYPE(TerritoryManager) // Tile data type, for easier accessing of coordinates struct Tile { Tile(u16 i, u16 j) : x(i), z(j) { } u16 x, z; }; // Floodfill templates that expand neighbours from a certain source onwards // (x, z) are the coordinates of the currently expanded tile // (nx, nz) are the coordinates of the current neighbour handled // The user of this floodfill should use "continue" on every neighbour that // shouldn't be expanded on its own. (without continue, an infinite loop will happen) # define FLOODFILL(i, j, code)\ do {\ const int NUM_NEIGHBOURS = 8;\ const int NEIGHBOURS_X[NUM_NEIGHBOURS] = {1,-1, 0, 0, 1,-1, 1,-1};\ const int NEIGHBOURS_Z[NUM_NEIGHBOURS] = {0, 0, 1,-1, 1,-1,-1, 1};\ std::queue openTiles;\ openTiles.emplace(i, j);\ while (!openTiles.empty())\ {\ u16 x = openTiles.front().x;\ u16 z = openTiles.front().z;\ openTiles.pop();\ for (int n = 0; n < NUM_NEIGHBOURS; ++n)\ {\ u16 nx = x + NEIGHBOURS_X[n];\ u16 nz = z + NEIGHBOURS_Z[n];\ /* Check the bounds, underflow will cause the values to be big again */\ if (nx >= tilesW || nz >= tilesH)\ continue;\ code\ openTiles.emplace(nx, nz);\ }\ }\ }\ while (false) /** * Compute the tile indexes on the grid nearest to a given point */ static void NearestTerritoryTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h) { entity_pos_t scale = Pathfinding::NAVCELL_SIZE * ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE; i = clamp((x / scale).ToInt_RoundToNegInfinity(), 0, w - 1); j = clamp((z / scale).ToInt_RoundToNegInfinity(), 0, h - 1); } void CCmpTerritoryManager::CalculateCostGrid() { if (m_CostGrid) return; CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; pass_class_t passClassTerritory = cmpPathfinder->GetPassabilityClass("default-terrain-only"); pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted"); const Grid& passGrid = cmpPathfinder->GetPassabilityGrid(); int tilesW = passGrid.m_W / NAVCELLS_PER_TERRITORY_TILE; int tilesH = passGrid.m_H / NAVCELLS_PER_TERRITORY_TILE; m_CostGrid = new Grid(tilesW, tilesH); + m_TerritoryTotalPassableCellCount = 0; for (int i = 0; i < tilesW; ++i) { for (int j = 0; j < tilesH; ++j) { u16 c = 0; for (u16 di = 0; di < NAVCELLS_PER_TERRITORY_TILE; ++di) for (u16 dj = 0; dj < NAVCELLS_PER_TERRITORY_TILE; ++dj) c |= passGrid.get( i * NAVCELLS_PER_TERRITORY_TILE + di, j * NAVCELLS_PER_TERRITORY_TILE + dj); if (c & passClassTerritory) m_CostGrid->set(i, j, m_ImpassableCost); else if (c & passClassUnrestricted) m_CostGrid->set(i, j, 255); // off the world; use maximum cost else + { m_CostGrid->set(i, j, 1); + ++m_TerritoryTotalPassableCellCount; + } } } } void CCmpTerritoryManager::CalculateTerritories() { if (m_Territories) return; PROFILE("CalculateTerritories"); // If the pathfinder hasn't been loaded (e.g. this is called during map initialisation), // abort the computation (and assume callers can cope with m_Territories == NULL) CalculateCostGrid(); if (!m_CostGrid) return; const u16 tilesW = m_CostGrid->m_W; const u16 tilesH = m_CostGrid->m_H; m_Territories = new Grid(tilesW, tilesH); + // Reset territory counts for all players + CmpPtr cmpPlayerManager(GetSystemEntity()); + if (cmpPlayerManager && cmpPlayerManager->GetNumPlayers() != m_TerritoryCellCounts.size()) + m_TerritoryCellCounts.resize(cmpPlayerManager->GetNumPlayers()); + for (u16& count : m_TerritoryCellCounts) + count = 0; + // Find all territory influence entities CComponentManager::InterfaceList influences = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence); // Split influence entities into per-player lists, ignoring any with invalid properties std::map > influenceEntities; for (const CComponentManager::InterfacePair& pair : influences) { entity_id_t ent = pair.first; CmpPtr cmpOwnership(GetSimContext(), ent); if (!cmpOwnership) continue; // Ignore Gaia and unassigned or players we can't represent player_id_t owner = cmpOwnership->GetOwner(); if (owner <= 0 || owner > TERRITORY_PLAYER_MASK) continue; influenceEntities[owner].push_back(ent); } // Store the overall best weight for comparison Grid bestWeightGrid(tilesW, tilesH); // store the root influences to mark territory as connected std::vector rootInfluenceEntities; for (const std::pair >& pair : influenceEntities) { // entityGrid stores the weight for a single entity, and is reset per entity Grid entityGrid(tilesW, tilesH); // playerGrid stores the combined weight of all entities for this player Grid playerGrid(tilesW, tilesH); u8 owner = (u8)pair.first; const std::vector& ents = pair.second; // With 2^16 entities, we're safe against overflows as the weight is also limited to 2^16 ENSURE(ents.size() < 1 << 16); // Compute the influence map of the current entity, then add it to the player grid for (entity_id_t ent : ents) { CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) continue; CmpPtr cmpTerritoryInfluence(GetSimContext(), ent); u32 weight = cmpTerritoryInfluence->GetWeight(); u32 radius = cmpTerritoryInfluence->GetRadius() / (Pathfinding::NAVCELL_SIZE * NAVCELLS_PER_TERRITORY_TILE).ToInt_RoundToNegInfinity(); if (weight == 0 || radius == 0) continue; u32 falloff = weight / radius; CFixedVector2D pos = cmpPosition->GetPosition2D(); u16 i, j; NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH); if (cmpTerritoryInfluence->IsRoot()) rootInfluenceEntities.push_back(ent); // Initialise the tile under the entity entityGrid.set(i, j, weight); if (weight > bestWeightGrid.get(i, j)) { bestWeightGrid.set(i, j, weight); m_Territories->set(i, j, owner); } // Expand influences outwards FLOODFILL(i, j, u32 dg = falloff * m_CostGrid->get(nx, nz); // diagonal neighbour -> multiply with approx sqrt(2) if (nx != x && nz != z) dg = (dg * 362) / 256; // Don't expand if new cost is not better than previous value for that tile // (arranged to avoid underflow if entityGrid.get(x, z) < dg) if (entityGrid.get(x, z) <= entityGrid.get(nx, nz) + dg) continue; // weight of this tile = weight of predecessor - falloff from predecessor u32 newWeight = entityGrid.get(x, z) - dg; u32 totalWeight = playerGrid.get(nx, nz) - entityGrid.get(nx, nz) + newWeight; playerGrid.set(nx, nz, totalWeight); entityGrid.set(nx, nz, newWeight); // if this weight is better than the best thus far, set the owner if (totalWeight > bestWeightGrid.get(nx, nz)) { bestWeightGrid.set(nx, nz, totalWeight); m_Territories->set(nx, nz, owner); } ); entityGrid.reset(); } } // Detect territories connected to a 'root' influence (typically a civ center) // belonging to their player, and mark them with the connected flag for (entity_id_t ent : rootInfluenceEntities) { // (These components must be valid else the entities wouldn't be added to this list) CmpPtr cmpOwnership(GetSimContext(), ent); CmpPtr cmpPosition(GetSimContext(), ent); CFixedVector2D pos = cmpPosition->GetPosition2D(); u16 i, j; NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH); u8 owner = (u8)cmpOwnership->GetOwner(); if (m_Territories->get(i, j) != owner) continue; m_Territories->set(i, j, owner | TERRITORY_CONNECTED_MASK); FLOODFILL(i, j, // Don't expand non-owner tiles, or tiles that already have a connected mask if (m_Territories->get(nx, nz) != owner) continue; m_Territories->set(nx, nz, owner | TERRITORY_CONNECTED_MASK); + if (m_CostGrid->get(nx, nz) < m_ImpassableCost) + ++m_TerritoryCellCounts[owner]; ); } } std::vector CCmpTerritoryManager::ComputeBoundaries() { PROFILE("ComputeBoundaries"); CalculateTerritories(); ENSURE(m_Territories); return CTerritoryBoundaryCalculator::ComputeBoundaries(m_Territories); } +u8 CCmpTerritoryManager::GetTerritoryPercentage(player_id_t player) +{ + if (player <= 0 && (size_t)player > m_TerritoryCellCounts.size()) + return 0; + + ENSURE(m_TerritoryTotalPassableCellCount > 0); + u8 percentage = (m_TerritoryCellCounts[player] * 100) / m_TerritoryTotalPassableCellCount; + ENSURE(percentage <= 100); + return percentage; +} + void CCmpTerritoryManager::UpdateBoundaryLines() { PROFILE("update boundary lines"); m_BoundaryLines.clear(); m_DebugBoundaryLineNodes.clear(); if (!CRenderer::IsInitialised()) return; std::vector boundaries = ComputeBoundaries(); CTextureProperties texturePropsBase("art/textures/misc/territory_border.png"); texturePropsBase.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsBase.SetMaxAnisotropy(2.f); CTexturePtr textureBase = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase); CTextureProperties texturePropsMask("art/textures/misc/territory_border_mask.png"); texturePropsMask.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsMask.SetMaxAnisotropy(2.f); CTexturePtr textureMask = g_Renderer.GetTextureManager().CreateTexture(texturePropsMask); CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return; for (size_t i = 0; i < boundaries.size(); ++i) { if (boundaries[i].points.empty()) continue; CColor color(1, 0, 1, 1); CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(boundaries[i].owner)); if (cmpPlayer) color = cmpPlayer->GetColor(); m_BoundaryLines.push_back(SBoundaryLine()); m_BoundaryLines.back().blinking = boundaries[i].blinking; m_BoundaryLines.back().color = color; m_BoundaryLines.back().overlay.m_SimContext = &GetSimContext(); m_BoundaryLines.back().overlay.m_TextureBase = textureBase; m_BoundaryLines.back().overlay.m_TextureMask = textureMask; m_BoundaryLines.back().overlay.m_Color = color; m_BoundaryLines.back().overlay.m_Thickness = m_BorderThickness; m_BoundaryLines.back().overlay.m_Closed = true; SimRender::SmoothPointsAverage(boundaries[i].points, m_BoundaryLines.back().overlay.m_Closed); SimRender::InterpolatePointsRNS(boundaries[i].points, m_BoundaryLines.back().overlay.m_Closed, m_BorderSeparation); std::vector& points = m_BoundaryLines.back().overlay.m_Coords; for (size_t j = 0; j < boundaries[i].points.size(); ++j) { points.push_back(boundaries[i].points[j].X); points.push_back(boundaries[i].points[j].Y); if (m_EnableLineDebugOverlays) { const size_t numHighlightNodes = 7; // highlight the X last nodes on either end to see where they meet (if closed) SOverlayLine overlayNode; if (j > boundaries[i].points.size() - 1 - numHighlightNodes) overlayNode.m_Color = CColor(1.f, 0.f, 0.f, 1.f); else if (j < numHighlightNodes) overlayNode.m_Color = CColor(0.f, 1.f, 0.f, 1.f); else overlayNode.m_Color = CColor(1.0f, 1.0f, 1.0f, 1.0f); overlayNode.m_Thickness = 1; SimRender::ConstructCircleOnGround(GetSimContext(), boundaries[i].points[j].X, boundaries[i].points[j].Y, 0.1f, overlayNode, true); m_DebugBoundaryLineNodes.push_back(overlayNode); } } } } void CCmpTerritoryManager::Interpolate(float frameTime, float UNUSED(frameOffset)) { m_AnimTime += frameTime; if (m_BoundaryLinesDirty) { UpdateBoundaryLines(); m_BoundaryLinesDirty = false; } for (size_t i = 0; i < m_BoundaryLines.size(); ++i) { if (m_BoundaryLines[i].blinking) { CColor c = m_BoundaryLines[i].color; c.a *= 0.2f + 0.8f * fabsf((float)cos(m_AnimTime * M_PI)); // TODO: should let artists tweak this m_BoundaryLines[i].overlay.m_Color = c; } } } void CCmpTerritoryManager::RenderSubmit(SceneCollector& collector) { for (size_t i = 0; i < m_BoundaryLines.size(); ++i) collector.Submit(&m_BoundaryLines[i].overlay); for (size_t i = 0; i < m_DebugBoundaryLineNodes.size(); ++i) collector.Submit(&m_DebugBoundaryLineNodes[i]); } player_id_t CCmpTerritoryManager::GetOwner(entity_pos_t x, entity_pos_t z) { u16 i, j; CalculateTerritories(); if (!m_Territories) return 0; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); return m_Territories->get(i, j) & TERRITORY_PLAYER_MASK; } std::vector CCmpTerritoryManager::GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected) { CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return std::vector(); std::vector ret(cmpPlayerManager->GetNumPlayers(), 0); CalculateTerritories(); if (!m_Territories) return ret; u16 i, j; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); // calculate the neighbours player_id_t thisOwner = m_Territories->get(i, j) & TERRITORY_PLAYER_MASK; u16 tilesW = m_Territories->m_W; u16 tilesH = m_Territories->m_H; // use a flood-fill algorithm that fills up to the borders and remembers the owners Grid markerGrid(tilesW, tilesH); markerGrid.set(i, j, true); FLOODFILL(i, j, if (markerGrid.get(nx, nz)) continue; // mark the tile as visited in any case markerGrid.set(nx, nz, true); int owner = m_Territories->get(nx, nz) & TERRITORY_PLAYER_MASK; if (owner != thisOwner) { if (owner == 0 || !filterConnected || (m_Territories->get(nx, nz) & TERRITORY_CONNECTED_MASK) != 0) ret[owner]++; // add player to the neighbour list when requested continue; // don't expand non-owner tiles further } ); return ret; } bool CCmpTerritoryManager::IsConnected(entity_pos_t x, entity_pos_t z) { u16 i, j; CalculateTerritories(); if (!m_Territories) return false; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); return (m_Territories->get(i, j) & TERRITORY_CONNECTED_MASK) != 0; } void CCmpTerritoryManager::SetTerritoryBlinking(entity_pos_t x, entity_pos_t z) { CalculateTerritories(); if (!m_Territories) return; u16 i, j; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); u16 tilesW = m_Territories->m_W; u16 tilesH = m_Territories->m_H; player_id_t thisOwner = m_Territories->get(i, j) & TERRITORY_PLAYER_MASK; FLOODFILL(i, j, u8 bitmask = m_Territories->get(nx, nz); if ((bitmask & TERRITORY_PLAYER_MASK) != thisOwner || (bitmask & TERRITORY_BLINKING_MASK)) continue; m_Territories->set(nx, nz, bitmask | TERRITORY_BLINKING_MASK); ); m_BoundaryLinesDirty = true; } TerritoryOverlay::TerritoryOverlay(CCmpTerritoryManager& manager) : TerrainTextureOverlay((float)Pathfinding::NAVCELLS_PER_TILE / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE), m_TerritoryManager(manager) { } void TerritoryOverlay::BuildTextureRGBA(u8* data, size_t w, size_t h) { for (size_t j = 0; j < h; ++j) { for (size_t i = 0; i < w; ++i) { SColor4ub color; u8 id = (m_TerritoryManager.m_Territories->get((int)i, (int)j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK); color = GetColor(id, 64); *data++ = color.R; *data++ = color.G; *data++ = color.B; *data++ = color.A; } } } #undef FLOODFILL Index: ps/trunk/source/simulation2/components/ICmpTerritoryManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpTerritoryManager.cpp (revision 16932) +++ ps/trunk/source/simulation2/components/ICmpTerritoryManager.cpp (revision 16933) @@ -1,29 +1,30 @@ /* Copyright (C) 2015 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 "ICmpTerritoryManager.h" #include "simulation2/system/InterfaceScripted.h" BEGIN_INTERFACE_WRAPPER(TerritoryManager) DEFINE_INTERFACE_METHOD_2("GetOwner", player_id_t, ICmpTerritoryManager, GetOwner, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("GetNeighbours", std::vector, ICmpTerritoryManager, GetNeighbours, entity_pos_t, entity_pos_t, bool) DEFINE_INTERFACE_METHOD_2("IsConnected", bool, ICmpTerritoryManager, IsConnected, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_2("SetTerritoryBlinking", void, ICmpTerritoryManager, SetTerritoryBlinking, entity_pos_t, entity_pos_t) +DEFINE_INTERFACE_METHOD_1("GetTerritoryPercentage", u8, ICmpTerritoryManager, GetTerritoryPercentage, player_id_t) END_INTERFACE_WRAPPER(TerritoryManager) Index: ps/trunk/source/simulation2/components/ICmpTerritoryManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpTerritoryManager.h (revision 16932) +++ ps/trunk/source/simulation2/components/ICmpTerritoryManager.h (revision 16933) @@ -1,78 +1,84 @@ /* Copyright (C) 2015 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_ICMPTERRITORYMANAGER #define INCLUDED_ICMPTERRITORYMANAGER #include "simulation2/system/Interface.h" #include "simulation2/helpers/Grid.h" #include "simulation2/helpers/Player.h" #include "simulation2/components/ICmpPosition.h" class ICmpTerritoryManager : public IComponent { public: virtual bool NeedUpdate(size_t* dirtyID) = 0; /** * Number of pathfinder navcells per territory tile. * Passability data is stored per navcell, but we probably don't need that much * resolution, and a lower resolution can make the boundary lines look prettier * and will take less memory, so we downsample the passability data. */ static const int NAVCELLS_PER_TERRITORY_TILE = 8; static const int TERRITORY_PLAYER_MASK = 0x1F; static const int TERRITORY_CONNECTED_MASK = 0x20; static const int TERRITORY_BLINKING_MASK = 0x40; static const int TERRITORY_PROCESSED_MASK = 0x80; //< For internal use; marks a tile as processed. /** * For each tile, the TERRITORY_PLAYER_MASK bits are player ID; * TERRITORY_CONNECTED_MASK is set if the tile is connected to a root object * (civ center etc). */ virtual const Grid& GetTerritoryGrid() = 0; /** * Get owner of territory at given position. * @return player ID of owner; 0 if neutral territory */ virtual player_id_t GetOwner(entity_pos_t x, entity_pos_t z) = 0; /** * get the number of neighbour tiles for per player for the selected position * @return A list with the number of neighbour tiles per player */ virtual std::vector GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected) = 0; /** * Get whether territory at given position is connected to a root object * (civ center etc) owned by that territory's player. */ virtual bool IsConnected(entity_pos_t x, entity_pos_t z) = 0; /** * Set a piece of territory to blinking. Must be updated on every territory calculation */ virtual void SetTerritoryBlinking(entity_pos_t x, entity_pos_t z) = 0; + /** + * Returns the percentage of the world controlled by a given player as defined by + * the number of territory cells the given player owns + */ + virtual u8 GetTerritoryPercentage(player_id_t player) = 0; + DECLARE_INTERFACE_TYPE(TerritoryManager) }; #endif // INCLUDED_ICMPTERRITORYMANAGER