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