Index: ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 19698)
+++ ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 19699)
@@ -1,654 +1,654 @@
// Ships respawn every few minutes, attack the closest warships, then patrol the sea.
// To prevent unlimited spawning of ships, no more than the amount of ships intended at a given time are spawned.
// Ships are filled or refilled with new units.
// The number ships, number of units per ship, as well as ratio of siege engines, champion and heroes
// increases with time, while keeping an individual and randomized composition for each ship.
// Each hero exists at most once per map.
// Every few minutes, equal amount of ships unload units at the sides of the river unless
// one side of the river was wiped from players.
// Siege engines attack defensive structures, units attack units then patrol that side of the river.
const showDebugLog = false;
var shipTemplate = "gaul_ship_trireme";
var siegeTemplate = "gaul_mechanical_siege_ram";
var heroTemplates = [
"gaul_hero_britomartus",
"gaul_hero_vercingetorix",
"gaul_hero_brennus"
];
var femaleTemplate = "gaul_support_female_citizen";
var healerTemplate = "gaul_support_healer_b";
var citizenInfantryTemplates = [
"gaul_infantry_javelinist_b",
"gaul_infantry_spearman_b",
"gaul_infantry_slinger_b"
];
var citizenCavalryTemplates = [
"gaul_cavalry_javelinist_b",
"gaul_cavalry_swordsman_b"
];
var citizenTemplates = [...citizenInfantryTemplates, ...citizenCavalryTemplates];
var championInfantryTemplates = [
"gaul_champion_fanatic",
"gaul_champion_infantry"
];
var championCavalryTemplates = [
"gaul_champion_cavalry"
];
var championTemplates = [...championInfantryTemplates, ...championCavalryTemplates];
var ccDefenders = [
{ "count": 8, "template": "units/" + pickRandom(citizenInfantryTemplates) },
{ "count": 8, "template": "units/" + pickRandom(championInfantryTemplates) },
{ "count": 4, "template": "units/" + pickRandom(championCavalryTemplates) },
{ "count": 4, "template": "units/" + healerTemplate },
{ "count": 5, "template": "units/" + femaleTemplate },
{ "count": 10, "template": "gaia/fauna_sheep" }
];
var gallicBuildingGarrison = [
{
"buildings": ["House"],
"units": [femaleTemplate, healerTemplate]
},
{
"buildings": ["CivCentre", "Temple"],
"units": championTemplates
},
{
"buildings": ["DefenseTower", "Outpost"],
"units": championInfantryTemplates
}
];
/**
* Notice if gaia becomes too strong, players will just turtle and try to outlast the players on the other side.
* However we want interaction and fights between the teams.
* This can be accomplished by not wiping out players buildings entirely.
*/
/**
* Time between two consecutive waves.
*/
var shipRespawnTime = () => randFloat(8, 10);
/**
* Limit of ships on the map when spawning them.
* Have at least two ships, so that both sides will be visited.
*/
var shipCount = (t, numPlayers) => Math.max(2, Math.round(Math.min(1.5, t / 10) * numPlayers));
/**
* Order all ships to ungarrison at the shoreline.
*/
var shipUngarrisonInterval = () => randFloat(5, 7);
/**
* Time between refillings of all ships with new soldiers.
*/
var shipFillInterval = () => randFloat(4, 5);
/**
* Total count of gaia attackers per shipload.
*/
var attackersPerShip = t => Math.min(30, Math.round(t * 2));
/**
* Likelihood of adding a non-existing hero at t minutes.
*/
var heroProbability = t => Math.max(0, Math.min(1, (t - 25) / 60));
/**
* Percent of healers to add per shipload after potentially adding a hero and siege engines.
*/
var healerRatio = t => randFloat(0, 0.1);
/**
* Percent of siege engines to add per shipload.
*/
var siegeRatio = t => t < 8 ? 0 : randFloat(0.03, 0.06);
/**
* Percent of champions to be added after spawning heroes, healers and siege engines.
* Rest will be citizen soldiers.
*/
var championRatio = t => Math.min(1, Math.max(0, (t - 25) / 75));
/**
* Ships and land units will queue attack orders for this amount of closest units.
*/
var targetCount = 3;
/**
* Number of trigger points to patrol when not having enemies to attack.
*/
var patrolCount = 5;
/**
* Which units ships should focus when attacking and patroling.
*/
var shipTargetClass = "WarShip";
/**
* Which entities siege engines should focus when attacking and patroling.
*/
var siegeTargetClass = "Defensive";
/**
* Which entities units should focus when attacking and patroling.
*/
var unitTargetClass = "Unit -Ship";
/**
* Ungarrison ships when being in this range of the target.
*/
var shipUngarrisonDistance = 50;
/**
* Currently formations are not working properly and enemies in vision range are often ignored.
* So only have a small chance of using formations.
*/
var formationProbability = 0.2;
var unitFormations = [
"box",
"battle_line",
"line_closed",
"column_closed"
];
/**
* Chance for the units at the meeting place to participate in the ritual.
*/
var ritualProbability = 0.75;
/**
* Units celebrating at the meeting place will perform one of these animations
* if idle and switch back when becoming idle again.
*/
var ritualAnimations = {
"female": ["attack_slaughter"],
"male": ["attack_capture", "promotion", "attack_slaughter"],
"healer": ["attack_capture", "promotion", "heal"]
};
var triggerPointShipSpawn = "A";
var triggerPointShipPatrol = "B";
var triggerPointUngarrisonLeft = "C";
var triggerPointUngarrisonRight = "D";
var triggerPointLandPatrolLeft = "E";
var triggerPointLandPatrolRight = "F";
/**
* Which playerID to use for the opposing gallic reinforcements.
*/
var gaulPlayer = 0;
Trigger.prototype.debugLog = function(txt)
{
if (showDebugLog)
print(
"DEBUG [" +
Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n");
};
/**
* Return a random amount of these templates whose sum is count.
*/
Trigger.prototype.RandomAttackerTemplates = function(templates, count)
{
let ratios = new Array(templates.length).fill(1).map(i => randFloat(0, 1));
let ratioSum = ratios.reduce((current, sum) => current + sum, 0);
let remainder = count;
let templateCounts = {};
for (let i in templates)
{
let currentCount = +i == templates.length - 1 ? remainder : Math.round(ratios[i] / ratioSum * count);
if (!currentCount)
continue;
templateCounts[templates[i]] = currentCount;
remainder -= currentCount;
}
if (remainder != 0)
warn("Not as many templates as expected: " + count + " vs " + uneval(templateCounts));
return templateCounts;
};
Trigger.prototype.GarrisonAllGallicBuildings = function(gaiaEnts)
{
this.debugLog("Garrisoning all gallic buildings");
for (let buildingGarrison of gallicBuildingGarrison)
for (let building of buildingGarrison.buildings)
this.SpawnAndGarrisonBuilding(gaiaEnts, building, buildingGarrison.units);
};
/**
* Garrisons all targetEnts that match the targetClass with newly spawned entities of the given template.
*/
Trigger.prototype.SpawnAndGarrisonBuilding = function(gaiaEnts, targetClass, templates)
{
for (let gaiaEnt of gaiaEnts)
{
let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass(targetClass))
continue;
let cmpGarrisonHolder = Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
continue;
let unitCounts = this.RandomAttackerTemplates(templates, cmpGarrisonHolder.GetCapacity());
this.debugLog("Garrisoning " + uneval(unitCounts) + " at " + targetClass);
for (let template in unitCounts)
for (let newEnt of TriggerHelper.SpawnUnits(gaiaEnt, "units/" + template, unitCounts[template], gaulPlayer))
Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder).Garrison(newEnt);
}
};
/**
* Spawn units of the template at each gaia Civic Center and set them to defensive.
*/
Trigger.prototype.SpawnCCDefenders = function(gaiaEnts)
{
this.debugLog("To defend CCs, spawning " + uneval(ccDefenders));
for (let gaiaEnt of gaiaEnts)
{
let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass("CivCentre"))
continue;
for (let ccDefender of ccDefenders)
for (let ent of TriggerHelper.SpawnUnits(gaiaEnt, ccDefender.template, ccDefender.count, gaulPlayer))
Engine.QueryInterface(ent, IID_UnitAI).SwitchToStance("defensive");
}
};
/**
* Remember most Humans present at the beginning of the match (before spawning any unit) and
* make them defensive.
*/
Trigger.prototype.StartCelticRitual = function(gaiaEnts)
{
for (let ent of gaiaEnts)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass("Human"))
continue;
if (randBool(ritualProbability))
this.ritualEnts.push(ent);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SwitchToStance("defensive");
}
this.DoRepeatedly(5 * 1000, "UpdateCelticRitual", {});
};
/**
* Play one of the given animations for most participants if and only if they are idle.
*/
Trigger.prototype.UpdateCelticRitual = function()
{
for (let ent of this.ritualEnts)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE")
continue;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
continue;
let animations = ritualAnimations[
cmpIdentity.HasClass("Healer") ? "healer" :
cmpIdentity.HasClass("Female") ? "female" : "male"];
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (!cmpVisual)
continue;
if (animations.indexOf(cmpVisual.GetAnimationName()) == -1)
cmpVisual.SelectAnimation(pickRandom(animations), false, 1, "");
}
};
/**
* Spawn ships with a unique attacker composition each until
* the number of ships is reached that is supposed to exist at the given time.
*/
Trigger.prototype.SpawnShips = function()
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
let shipSpawnCount = shipCount(time, numPlayers) - this.ships.length;
this.debugLog("Spawning " + shipSpawnCount + " ships");
while (this.ships.length < shipSpawnCount)
this.ships.push(TriggerHelper.SpawnUnits(pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)), "units/" + shipTemplate, 1, gaulPlayer)[0]);
for (let ship of this.ships)
this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships");
this.DoAfterDelay(shipRespawnTime() * 60 * 1000, "SpawnShips", {});
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.fillShipsTimer);
this.FillShips();
};
Trigger.prototype.FillShips = function()
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
let attackerCount = attackersPerShip(time);
for (let ship of this.ships)
{
let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
continue;
let remainder = Math.max(0, attackerCount - cmpGarrisonHolder.GetEntities().length);
let toSpawn = [];
let siegeCount = Math.round(siegeRatio(time) * remainder);
if (siegeCount)
toSpawn.push({ "template": siegeTemplate, "count": siegeCount });
remainder -= siegeCount;
let heroTemplate = pickRandom(heroTemplates.filter(hTemp => this.heroes.every(hero => hTemp != hero.template)));
if (heroTemplate && remainder && randBool(heroProbability(time)))
{
toSpawn.push({ "template": heroTemplate, "count": 1, "hero": true });
--remainder;
}
let healerCount = Math.round(healerRatio(time) * remainder);
if (healerCount)
toSpawn.push({ "template": healerTemplate, "count": healerCount });
remainder -= healerCount;
let championCount = Math.round(championRatio(time) * remainder);
let championTemplateCounts = this.RandomAttackerTemplates(championTemplates, championCount);
for (let template in championTemplateCounts)
{
let count = championTemplateCounts[template];
toSpawn.push({ "template": template, "count": count });
championCount -= count;
remainder -= count;
}
let citizenTemplateCounts = this.RandomAttackerTemplates(citizenTemplates, remainder);
for (let template in citizenTemplateCounts)
{
let count = citizenTemplateCounts[template];
toSpawn.push({ "template": template, "count": count });
remainder -= count;
}
this.debugLog("Filling ship " + ship + " with " + uneval(toSpawn));
if (remainder != 0)
warn("Didn't spawn as many attackers as were intended (" + remainder + " remaining)");
for (let spawn of toSpawn)
{
// Don't use TriggerHelper.SpawnUnits here because that is too slow,
// needlessly trying all spawn points near the ships footprint which all fail
for (let i = 0; i < spawn.count; ++i)
{
let ent = Engine.AddEntity("units/" + spawn.template);
Engine.QueryInterface(ent, IID_Ownership).SetOwner(gaulPlayer);
if (spawn.hero)
this.heroes.push({ "template": spawn.template, "ent": ent });
cmpGarrisonHolder.Garrison(ent);
}
}
}
this.fillShipsTimer = this.DoAfterDelay(shipFillInterval() * 60 * 1000, "FillShips", {});
};
/**
* Attack the closest enemy ships around, then patrol the sea.
*/
Trigger.prototype.AttackAndPatrol = function(attackers, targetClass, triggerPointRef, debugName)
{
if (!attackers.length)
return;
let allTargets = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities().filter(ent => {
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClass);
});
let targets = allTargets.sort((ent1, ent2) =>
DistanceBetweenEntities(attackers[0], ent1) - DistanceBetweenEntities(attackers[0], ent2)).slice(0, targetCount);
this.debugLog(debugName + " " + uneval(attackers) + " attack " + uneval(targets));
ProcessCommand(gaulPlayer, {
"type": "stance",
"entities": attackers,
"name": "violent",
"queued": true
});
for (let target of targets)
ProcessCommand(gaulPlayer, {
"type": "attack",
"entities": attackers,
"target": target,
"queued": true,
"allowCapture": false
});
let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount);
this.debugLog(debugName + " " + uneval(attackers) + " patrol to " + uneval(patrolTargets));
for (let patrolTarget of patrolTargets)
{
let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D();
ProcessCommand(gaulPlayer, {
"type": "patrol",
"entities": attackers,
"x": pos.x,
"z": pos.y,
"targetClasses": {
"attack": [targetClass]
},
"queued": true,
"allowCapture": false
});
}
};
/**
* Order all ships to abort naval warfare and move to the shoreline all few minutes.
*/
Trigger.prototype.UngarrisonShipsOrder = function()
{
// To avoid unloading unlimited amounts of units on empty riversides,
// only ungarrison on riversides where player buildings exist
let ungarrisonLeft = false;
let ungarrisonRight = false;
- let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetTilesPerSide() * 4;
+ let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize();
for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities())
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass("Structure"))
continue;
if (Engine.QueryInterface(ent, IID_Position).GetPosition2D().x < mapSize / 2)
ungarrisonLeft = true;
else
ungarrisonRight = true;
if (ungarrisonLeft && ungarrisonRight)
break;
}
if (!ungarrisonLeft && !ungarrisonRight)
return;
// Determine which ships should ungarrison on which side of the river
let shipsLeft = [];
let shipsRight = [];
if (ungarrisonLeft && ungarrisonRight)
{
shipsLeft = shuffleArray(this.ships).slice(0, Math.round(this.ships.length / 2));
shipsRight = this.ships.filter(ship => shipsLeft.indexOf(ship) == -1);
}
else if (ungarrisonLeft)
shipsLeft = this.ships;
else if (ungarrisonRight)
shipsRight = this.ships;
// Determine which ships should ungarrison and patrol at which trigger point names
let sides = [];
if (shipsLeft.length)
sides.push({
"ships": shipsLeft,
"ungarrisonPointRef": triggerPointUngarrisonLeft,
"landPointRef": triggerPointLandPatrolLeft
});
if (shipsRight.length)
sides.push({
"ships": shipsRight,
"ungarrisonPointRef": triggerPointUngarrisonRight,
"landPointRef": triggerPointLandPatrolRight
});
// Order those ships to move to a randomly chosen trigger point on the determined
// side of the river. Remember that chosen ungarrison point and the name of the
// trigger points where the ungarrisoned units should patrol afterwards.
for (let side of sides)
for (let ship of side.ships)
{
let ungarrisonPoint = pickRandom(this.GetTriggerPoints(side.ungarrisonPointRef));
let ungarrisonPos = Engine.QueryInterface(ungarrisonPoint, IID_Position).GetPosition2D();
this.debugLog("Ship " + ship + " will ungarrison at " + side.ungarrisonPointRef +
" (" + ungarrisonPos.x + "," + ungarrisonPos.y + ")");
Engine.QueryInterface(ship, IID_UnitAI).Walk(ungarrisonPos.x, ungarrisonPos.y, false);
this.shipTarget[ship] = { "landPointRef": side.landPointRef, "ungarrisonPoint": ungarrisonPoint };
}
this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
};
/**
* Check frequently whether the ships are close enough to unload at the shoreline.
*/
Trigger.prototype.CheckShipRange = function()
{
for (let ship of this.ships)
{
if (!this.shipTarget[ship] || DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance)
continue;
let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
continue;
let attackers = cmpGarrisonHolder.GetEntities();
let siegeEngines = attackers.filter(ent => Engine.QueryInterface(ent, IID_Identity).HasClass("Siege"));
let others = attackers.filter(ent => siegeEngines.indexOf(ent) == -1);
this.debugLog("Ungarrisoning ship " + ship + " at " + uneval(this.shipTarget[ship]));
cmpGarrisonHolder.UnloadAll();
if (randBool(formationProbability))
ProcessCommand(gaulPlayer, {
"type": "formation",
"entities": others,
"name": "formations/" + pickRandom(unitFormations)
});
this.AttackAndPatrol(siegeEngines, siegeTargetClass, this.shipTarget[ship].landPointRef, "Siege");
this.AttackAndPatrol(others, unitTargetClass, this.shipTarget[ship].landPointRef, "Units");
delete this.shipTarget[ship];
this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships");
}
};
Trigger.prototype.DanubiusOwnershipChange = function(data)
{
if (data.to != -1)
return;
let shipIdx = this.ships.indexOf(data.entity);
if (shipIdx != -1)
{
this.debugLog("Ship " + data.entity + " sunk");
this.ships.splice(shipIdx, 1);
}
let ritualIdx = this.ritualEnts.indexOf(data.entity);
if (ritualIdx != -1)
this.ritualEnts.splice(ritualIdx, 1);
let heroIdx = this.heroes.findIndex(hero => hero.ent == data.entity);
if (ritualIdx != -1)
this.heroes.splice(heroIdx, 1);
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
let gaiaEnts = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0);
cmpTrigger.ritualEnts = [];
// To prevent spawning more than the limits, track IDs of current entities
cmpTrigger.ships = [];
cmpTrigger.heroes = [];
// Maps from gaia ship entity ID to ungarrison trigger point entity ID and land patrol triggerpoint name
cmpTrigger.shipTarget = {};
cmpTrigger.fillShipsTimer = undefined;
cmpTrigger.StartCelticRitual(gaiaEnts);
cmpTrigger.GarrisonAllGallicBuildings(gaiaEnts);
cmpTrigger.SpawnCCDefenders(gaiaEnts);
cmpTrigger.SpawnShips();
cmpTrigger.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
cmpTrigger.DoRepeatedly(5 * 1000, "CheckShipRange", {});
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DanubiusOwnershipChange", { "enabled": true });
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 19698)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 19699)
@@ -1,2021 +1,2021 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised for the biggest part
// So most of the attributes shouldn't be serialized
// Return an object with a small selection of deterministic data
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
let cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// Work out what phase we are in
let phase = "";
let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
// store player ally/neutral/enemy data as arrays
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(playerEnt),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(playerEnt)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
- ret.mapSize = 4 * cmpTerrain.GetTilesPerSide();
+ ret.mapSize = cmpTerrain.GetMapSize();
// Add timeElapsed
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
// Add ceasefire info
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
// Add the game type and allied victory
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.gameType = cmpEndGameManager.GetGameType();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
// Add Resource Codes, untranslated names and AI Analysis
ret.resources = {
"codes": Resources.GetCodes(),
"names": Resources.GetNames(),
"aiInfluenceGroups": {}
};
for (let res of ret.resources.codes)
ret.resources.aiInfluenceGroups[res] = Resources.GetResource(res).aiAnalysisInfluenceGroup;
// Add basic statistics to each player
for (let i = 0; i < numPlayers; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
// Get basic simulation info
let ret = this.GetSimulationState();
// Add statistics to each player
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let n = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < n; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
else
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"template": template,
"alertRaiser": null,
"builder": null,
"canGarrison": null,
"identity": null,
"fogging": null,
"foundation": null,
"garrisonHolder": null,
"gate": null,
"guard": null,
"market": null,
"mirage": null,
"pack": null,
"upgrade" : null,
"player": -1,
"position": null,
"production": null,
"rallyPoint": null,
"resourceCarrying": null,
"rotation": null,
"trader": null,
"unitAI": null,
"visibility": null,
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
ret.position = cmpPosition.GetPosition();
ret.rotation = cmpPosition.GetRotation();
}
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints();
ret.needsHeal = !cmpHealth.IsUnhealable();
ret.canDelete = !cmpHealth.IsUndeletable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval"),
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress(),
};
var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades" : cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo()
};
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
if (cmpFogging)
ret.fogging = {
"mirage": cmpFogging.IsMiraged(player) ? cmpFogging.GetMirage(player) : null
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage(),
"numBuilders": cmpFoundation.GetNumBuilders()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() };
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"possibleStances": cmpUnitAI.GetPossibleStances(),
"isIdle":cmpUnitAI.IsIdle(),
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities(),
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked(),
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = {
"level": cmpAlertRaiser.GetLevel(),
"canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
"hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
return ret;
};
/**
* Get additionnal entity info, rarely used in the gui
*/
GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
{
let ret = {
"armour": null,
"attack": null,
"buildingAI": null,
"heal": null,
"isBarterMarket": null,
"loot": null,
"obstruction": null,
"turretParent":null,
"promotion": null,
"repairRate": null,
"buildRate": null,
"resourceDropsite": null,
"resourceGatherRates": null,
"resourceSupply": null,
"resourceTrickle": null,
"speed": null,
};
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = cmpAttack.GetAttackStrengths(type);
ret.attack[type].splash = cmpAttack.GetSplashDamage(type);
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// not a ranged attack, set some defaults
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the range in front of it, no spread. So angle = 0
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
}
else
{
// not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
}
let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
ret.armour = cmpArmour.GetArmourStrengths();
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras)
ret.auras = cmpAuras.GetDescriptions();
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (cmpObstruction)
ret.obstruction = {
"controlGroup": cmpObstruction.GetControlGroup(),
"controlGroup2": cmpObstruction.GetControlGroup2(),
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpRepairable = Engine.QueryInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairRate = cmpRepairable.GetRepairRate();
let cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.buildRate = cmpFoundation.GetBuildRate();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"hp": cmpHeal.GetHP(),
"range": cmpHeal.GetRange().max,
"rate": cmpHeal.GetRate(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
let resources = cmpLoot.GetResources();
ret.loot = {
"xp": cmpLoot.GetXp()
};
for (let res of Resources.GetCodes())
ret.loot[res] = resources[res];
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
{
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetTimer(),
"rates": {}
};
let rates = cmpResourceTrickle.GetRates();
for (let res in rates)
ret.resourceTrickle.rates[res] = rates[res];
}
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetRunSpeed()
};
return ret;
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, name)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(name);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, player, aurasTemplate, Resources);
// Add aura name and description loaded from JSON file
let auraNames = template.Auras._string.split(/\s+/);
let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
for (let name of auraNames)
aurasTemplate[name] = cmpDataTemplateManager.GetAuraTemplate(name);
return GetTemplateDataHelper(template, player, aurasTemplate, Resources);
};
GuiInterface.prototype.GetTechnologyData = function(player, data)
{
let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
let template = cmpDataTemplateManager.GetTechnologyTemplate(data.name);
if (!template)
{
warn("Tried to get data for invalid technology: " + data.name);
return null;
}
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
return GetTechnologyDataHelper(template, data.civ || cmpPlayer.GetCiv(), Resources);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
// Checks whether the requirements for this technology have been met
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
// Returns technologies that are being actively researched, along with
// which entity is researching them and how far along the research is.
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech in cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
else
ret[tech].progress = 0;
}
return ret;
};
// Returns the battle state of the player.
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
// Returns a list of ongoing attacks against the player.
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
};
// Used to show a red square over GUI elements you can't yet afford.
GuiInterface.prototype.GetNeededResources = function(player, data)
{
return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost);
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default
if (notification.players == undefined)
{
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
notification.players = [-1];
for (let i = 1; i < numPlayers; ++i)
notification.players.push(i);
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// filter on players and time, since the delete timer might be executed with a delay
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
return QueryPlayerIDInterface(wantedPlayer).GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
// GetLastFormationName is named in a strange way as it (also) is
// the value of the current formation (see Formation.js LoadFormation)
if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
return true;
}
return false;
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
let playerColors = {}; // cache of owner -> color map
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color:
let owner = -1;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r":1, "g":1, "b":1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeVisualization = Engine.QueryInterface(ent, IID_RangeVisualization);
if (!cmpRangeVisualization || player != owner && player != -1)
continue;
cmpRangeVisualization.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return [...this.entsWithAuraAndStatusBars];
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeVisualization = Engine.QueryInterface(ent, IID_RangeVisualization);
if (cmpRangeVisualization)
cmpRangeVisualization.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location)
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
if (pos)
{
// Only update the position if we changed it (cmd.queued is set)
if ("queued" in cmd)
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
else
cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": [],
};
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
// Move the preview into the right location
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
// Set it to a red shade if this is an invalid location
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* 'populationBonus': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
// --------------------------------------------------------------------------------
// do some entity cache management and check for snapping
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// we're clearing the preview, clear the entity cache and bail
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// keep template data around
}
return false;
}
else
{
// Move all existing cached entities outside of the world and reset their use count
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before
for (let type in wallSet.templates)
{
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, tpl),
};
// ensure that the loaded template data contains a wallPiece component
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
}
// prevent division by zero errors further on if the start and end positions are the same
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// clear the single-building preview entity (we'll be rolling our own)
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// calculate wall placement and position preview entities
let result = {
"pieces": [],
"cost": { "population": 0, "populationBonus": 0, "time": 0 },
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length > 0 && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)],
"excludeFromResult": true, // preview only, must not appear in the result
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle)
});
}
if (end.pos)
{
// Analogous to the starting side case above
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []);
previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle)
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
// allocate new entity
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
// reuse an existing one
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// move piece to right location
// TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
// check whether this wall piece can be validly positioned here
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region
// TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
let visible = (cmpRangeManager.GetLosVisibility(ent, player) != "hidden");
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement
validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success);
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest
// (TODO: break unlikely ties by choosing the lowest entity ID)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (template.BuildRestrictions.Category == "Dock")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
var bucket = filtered.bucket;
if(bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if(!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
{
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
}
else if (!firstMarket)
{
result = { "type": "set first" };
}
else if (!secondMarket)
{
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
}
else
{
// Else both markets are not null and target is different from them
result = { "type": "set first" };
}
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
return QueryPlayerIDInterface(player).GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
// trusted and indicates the player associated with the current client; no data should
// be returned unless this player is meant to be able to see it.)
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetExtendedEntityState": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"GetTechnologyData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
else
throw new Error("Invalid GuiInterface Call name \""+name+"\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js (revision 19698)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js (revision 19699)
@@ -1,354 +1,354 @@
// (A serious implementation of this might want to use C++ instead of JS
// for performance; this is just for fun.)
const SHORT_FINAL = 2.5;
function UnitMotionFlying() {}
UnitMotionFlying.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
UnitMotionFlying.prototype.Init = function()
{
this.hasTarget = false;
this.reachedTarget = false;
this.targetX = 0;
this.targetZ = 0;
this.targetMinRange = 0;
this.targetMaxRange = 0;
this.speed = 0;
this.landing = false;
this.onGround = true;
this.pitch = 0;
this.roll = 0;
this.waterDeath = false;
this.passabilityClass = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).GetPassabilityClass(this.template.PassabilityClass);
};
UnitMotionFlying.prototype.OnUpdate = function(msg)
{
var turnLength = msg.turnLength;
if (!this.hasTarget)
return;
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
var pos = cmpPosition.GetPosition();
var angle = cmpPosition.GetRotation().y;
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
var ground = Math.max(cmpTerrain.GetGroundLevel(pos.x, pos.z), cmpWaterManager.GetWaterLevel(pos.x, pos.z));
var newangle = angle;
var canTurn = true;
if (this.landing)
{
if (this.speed > 0 && this.onGround)
{
if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater == "true")
this.waterDeath = true;
this.pitch = 0;
// Deaccelerate forwards...at a very reduced pace.
if (this.waterDeath)
this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate * 10);
else
this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate);
canTurn = false;
// Clamp to ground if below it, or descend if above
if (pos.y < ground)
pos.y = ground;
else if (pos.y > ground)
pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
}
else if (this.speed == 0 && this.onGround)
{
if (this.waterDeath && cmpHealth)
cmpHealth.Kill();
else
{
this.pitch = 0;
// We've stopped.
if (cmpGarrisonHolder)
cmpGarrisonHolder.AllowGarrisoning(true,"UnitMotionFlying");
canTurn = false;
this.hasTarget = false;
this.landing = false;
// summon planes back from the edge of the map
- var terrainSize = cmpTerrain.GetTilesPerSide() * 4;
+ var terrainSize = cmpTerrain.GetMapSize();
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager.GetLosCircular())
{
var mapRadius = terrainSize/2;
var x = pos.x - mapRadius;
var z = pos.z - mapRadius;
var div = (mapRadius - 12) / Math.sqrt(x*x + z*z);
if (div < 1)
{
pos.x = mapRadius + x*div;
pos.z = mapRadius + z*div;
newangle += Math.PI;
}
}
else
{
pos.x = Math.max(Math.min(pos.x, terrainSize - 12), 12);
pos.z = Math.max(Math.min(pos.z, terrainSize - 12), 12);
newangle += Math.PI;
}
}
}
else
{
// Final Approach
// We need to slow down to land!
this.speed = Math.max(this.template.LandingSpeed, this.speed - turnLength * this.template.SlowingRate);
canTurn = false;
var targetHeight = ground;
// Steep, then gradual descent.
if ((pos.y - targetHeight) / this.template.FlyingHeight > 1 / SHORT_FINAL)
this.pitch = - Math.PI / 18;
else
this.pitch = Math.PI / 18;
var descentRate = ((pos.y - targetHeight) / this.template.FlyingHeight * this.template.ClimbRate + SHORT_FINAL) * SHORT_FINAL;
if (pos.y < targetHeight)
pos.y = Math.max(targetHeight, pos.y + turnLength * descentRate);
else if (pos.y > targetHeight)
pos.y = Math.max(targetHeight, pos.y - turnLength * descentRate);
if (targetHeight == pos.y)
{
this.onGround = true;
if (targetHeight == cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater)
this.waterDeath = true;
}
}
}
else
{
// If we haven't reached max speed yet then we're still on the ground;
// otherwise we're taking off or flying
// this.onGround in case of a go-around after landing (but not fully stopped)
if (this.speed < this.template.TakeoffSpeed && this.onGround)
{
if (cmpGarrisonHolder)
cmpGarrisonHolder.AllowGarrisoning(false,"UnitMotionFlying");
this.pitch = 0;
// Accelerate forwards
this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
canTurn = false;
// Clamp to ground if below it, or descend if above
if (pos.y < ground)
pos.y = ground;
else if (pos.y > ground)
pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
}
else
{
this.onGround = false;
// Climb/sink to max height above ground
this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
var targetHeight = ground + (+this.template.FlyingHeight);
if (Math.abs(pos.y-targetHeight) > this.template.FlyingHeight/5)
{
this.pitch = Math.PI / 9;
canTurn = false;
}
else
this.pitch = 0;
if (pos.y < targetHeight)
pos.y = Math.min(targetHeight, pos.y + turnLength * this.template.ClimbRate);
else if (pos.y > targetHeight)
{
pos.y = Math.max(targetHeight, pos.y - turnLength * this.template.ClimbRate);
this.pitch = -1 * this.pitch;
}
}
}
// If we're in range of the target then tell people that we've reached it
// (TODO: quantisation breaks this)
var distFromTarget = Math.sqrt(Math.pow(this.targetX - pos.x, 2) + Math.pow(this.targetZ - pos.z, 2));
if (!this.reachedTarget && this.targetMinRange <= distFromTarget && distFromTarget <= this.targetMaxRange)
{
this.reachedTarget = true;
Engine.PostMessage(this.entity, MT_MotionChanged, { "starting": false, "error": false });
}
// If we're facing away from the target, and are still fairly close to it,
// then carry on going straight so we overshoot in a straight line
var isBehindTarget = ((this.targetX - pos.x) * Math.sin(angle) + (this.targetZ - pos.z) * Math.cos(angle) < 0);
// Overshoot the target: carry on straight
if (isBehindTarget && distFromTarget < this.template.MaxSpeed * this.template.OvershootTime)
canTurn = false;
if (canTurn)
{
// Turn towards the target
var targetAngle = Math.atan2(this.targetX - pos.x, this.targetZ - pos.z);
var delta = targetAngle - angle;
// Wrap delta to -pi..pi
delta = (delta + Math.PI) % (2*Math.PI); // range -2pi..2pi
if (delta < 0) delta += 2*Math.PI; // range 0..2pi
delta -= Math.PI; // range -pi..pi
// Clamp to max rate
var deltaClamped = Math.min(Math.max(delta, -this.template.TurnRate * turnLength), this.template.TurnRate * turnLength);
// Calculate new orientation, in a peculiar way in order to make sure the
// result gets close to targetAngle (rather than being n*2*pi out)
newangle = targetAngle + deltaClamped - delta;
if (newangle - angle > Math.PI / 18)
this.roll = Math.PI / 9;
else if (newangle - angle < -Math.PI / 18)
this.roll = - Math.PI / 9;
else
this.roll = newangle - angle;
}
else
this.roll = 0;
pos.x += this.speed * turnLength * Math.sin(angle);
pos.z += this.speed * turnLength * Math.cos(angle);
cmpPosition.SetHeightFixed(pos.y);
cmpPosition.TurnTo(newangle);
cmpPosition.SetXZRotation(this.pitch, this.roll);
cmpPosition.MoveTo(pos.x, pos.z);
};
UnitMotionFlying.prototype.MoveToPointRange = function(x, z, minRange, maxRange)
{
this.hasTarget = true;
this.landing = false;
this.reachedTarget = false;
this.targetX = x;
this.targetZ = z;
this.targetMinRange = minRange;
this.targetMaxRange = maxRange;
return true;
};
UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRange)
{
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return false;
var targetPos = cmpTargetPosition.GetPosition2D();
this.hasTarget = true;
this.reachedTarget = false;
this.targetX = targetPos.x;
this.targetZ = targetPos.y;
this.targetMinRange = minRange;
this.targetMaxRange = maxRange;
return true;
};
UnitMotionFlying.prototype.IsInPointRange = function(x, y, minRange, maxRange)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
var pos = cmpPosition.GetPosition2D();
var distFromTarget = Math.sqrt(Math.pow(x - pos.x, 2) + Math.pow(y - pos.y, 2));
if (minRange <= distFromTarget && distFromTarget <= maxRange)
return true;
return false;
};
UnitMotionFlying.prototype.IsInTargetRange = function(target, minRange, maxRange)
{
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return false;
var targetPos = cmpTargetPosition.GetPosition2D();
return this.IsInPointRange(targetPos.x, targetPos.y, minRange, maxRange);
};
UnitMotionFlying.prototype.GetWalkSpeed = function()
{
return +this.template.MaxSpeed;
};
UnitMotionFlying.prototype.SetSpeed = function()
{
// ignore this, the speed is always the walk speed
};
UnitMotionFlying.prototype.GetRunSpeed = function()
{
return this.GetWalkSpeed();
};
UnitMotionFlying.prototype.GetCurrentSpeed = function()
{
return this.speed;
};
UnitMotionFlying.prototype.GetPassabilityClassName = function()
{
return this.template.PassabilityClass;
};
UnitMotionFlying.prototype.GetPassabilityClass = function()
{
return this.passabilityClass;
};
UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z)
{
// Ignore this - angle is controlled by the target-seeking code instead
};
UnitMotionFlying.prototype.SetFacePointAfterMove = function()
{
// Ignore this - angle is controlled by the target-seeking code instead
};
UnitMotionFlying.prototype.StopMoving = function()
{
//Invert
if (!this.waterDeath)
this.landing = !this.landing;
};
UnitMotionFlying.prototype.SetDebugOverlay = function(enabled)
{
};
Engine.RegisterComponentType(IID_UnitMotion, "UnitMotionFlying", UnitMotionFlying);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js (revision 19698)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js (revision 19699)
@@ -1,146 +1,146 @@
Engine.LoadComponentScript("UnitMotionFlying.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
let entity = 1;
let target = 2;
let height = 5;
AddMock(SYSTEM_ENTITY, IID_Pathfinder, {
GetPassabilityClass: (name) => 1 << 8
});
let cmpUnitMotionFlying = ConstructComponent(entity, "UnitMotionFlying", {
"MaxSpeed": 1.0,
"TakeoffSpeed": 0.5,
"LandingSpeed": 0.5,
"AccelRate": 0.0005,
"SlowingRate": 0.001,
"BrakingRate": 0.0005,
"TurnRate": 0.1,
"OvershootTime": 10,
"FlyingHeight": 100,
"ClimbRate": 0.1,
"DiesInWater": false,
"PassabilityClass": "unrestricted"
});
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetWalkSpeed(), 1.0);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetRunSpeed(), 1.0);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
cmpUnitMotionFlying.SetSpeed(2.0);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetWalkSpeed(), 1.0);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetRunSpeed(), 1.0);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClassName(), "unrestricted");
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClass(), 1 << 8);
AddMock(entity, IID_Position, {
"IsInWorld": () => true,
"GetPosition2D": () => { return { "x": 50, "y": 100 }; },
"GetPosition": () => { return { "x": 50, "y": height, "z": 100 }; },
"GetRotation": () => { return { "y": 3.14 }; },
"SetHeightFixed": (y) => height = y,
"TurnTo": () => {},
"SetXZRotation": () => {},
"MoveTo": () => {}
});
AddMock(target, IID_Position, {
"IsInWorld": () => true,
"GetPosition2D": () => { return { "x": 100, "y": 200 }; }
});
TS_ASSERT_EQUALS(cmpUnitMotionFlying.IsInTargetRange(target, 10, 112), true);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.IsInTargetRange(target, 50, 111), false);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.IsInTargetRange(target, 112, 200), false);
AddMock(entity, IID_GarrisonHolder, {
"AllowGarrisoning": () => {}
});
AddMock(entity, IID_Health, {
});
AddMock(entity, IID_RangeManager, {
"GetLosCircular": () => true
});
AddMock(entity, IID_Terrain, {
"GetGroundLevel": () => 4,
- "GetTilesPerSide": () => 5
+ "GetMapSize": () => 20
});
AddMock(entity, IID_WaterManager, {
"GetWaterLevel": () => 5
});
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToTargetRange(target, 0, 10), true);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToPointRange(100, 200, 0, 20), true);
// Take Off
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.25);
TS_ASSERT_EQUALS(height, 5);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.5);
TS_ASSERT_EQUALS(height, 5);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.5);
TS_ASSERT_EQUALS(height, 5);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.75);
TS_ASSERT_EQUALS(height, 55);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1);
TS_ASSERT_EQUALS(height, 105);
// Fly
cmpUnitMotionFlying.OnUpdate({ "turnLength": 100 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1);
TS_ASSERT_EQUALS(height, 105);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1);
TS_ASSERT_EQUALS(height, 105);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1);
TS_ASSERT_EQUALS(height, 105);
// Land
cmpUnitMotionFlying.StopMoving();
cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1);
TS_ASSERT_EQUALS(height, 105);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.5);
TS_ASSERT_EQUALS(height, 5);
// Slide
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.25);
TS_ASSERT_EQUALS(height, 5);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.25);
TS_ASSERT_EQUALS(height, 5);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
TS_ASSERT_EQUALS(height, 5);
// Stay
cmpUnitMotionFlying.OnUpdate({ "turnLength": 300 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
TS_ASSERT_EQUALS(height, 5);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
TS_ASSERT_EQUALS(height, 5);
cmpUnitMotionFlying.OnUpdate({ "turnLength": 900 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
TS_ASSERT_EQUALS(height, 5);
Index: ps/trunk/source/simulation2/components/CCmpTerrain.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpTerrain.cpp (revision 19698)
+++ ps/trunk/source/simulation2/components/CCmpTerrain.cpp (revision 19699)
@@ -1,159 +1,164 @@
/* Copyright (C) 2017 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 "ICmpTerrain.h"
#include "ICmpObstructionManager.h"
#include "ICmpRangeManager.h"
#include "simulation2/MessageTypes.h"
#include "graphics/Terrain.h"
#include "renderer/Renderer.h"
#include "renderer/WaterManager.h"
#include "maths/Vector3D.h"
class CCmpTerrain : public ICmpTerrain
{
public:
static void ClassInit(CComponentManager& UNUSED(componentManager))
{
}
DEFAULT_COMPONENT_ALLOCATOR(Terrain)
CTerrain* m_Terrain; // not null
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_Terrain = &GetSimContext().GetTerrain();
}
virtual void Deinit()
{
}
virtual void Serialize(ISerializer& UNUSED(serialize))
{
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize))
{
Init(paramNode);
}
virtual bool IsLoaded() const
{
return m_Terrain->GetVerticesPerSide() != 0;
}
virtual CFixedVector3D CalcNormal(entity_pos_t x, entity_pos_t z) const
{
CFixedVector3D normal;
m_Terrain->CalcNormalFixed((x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), (z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), normal);
return normal;
}
virtual CVector3D CalcExactNormal(float x, float z) const
{
return m_Terrain->CalcExactNormal(x, z);
}
virtual entity_pos_t GetGroundLevel(entity_pos_t x, entity_pos_t z) const
{
// TODO: this can crash if the terrain heightmap isn't initialised yet
return m_Terrain->GetExactGroundLevelFixed(x, z);
}
virtual float GetExactGroundLevel(float x, float z) const
{
return m_Terrain->GetExactGroundLevel(x, z);
}
virtual u16 GetTilesPerSide() const
{
ssize_t tiles = m_Terrain->GetTilesPerSide();
if (tiles == -1)
return 0;
ENSURE(1 <= tiles && tiles <= 65535);
return (u16)tiles;
}
+ virtual u32 GetMapSize() const
+ {
+ return GetTilesPerSide() * TERRAIN_TILE_SIZE;
+ }
+
virtual u16 GetVerticesPerSide() const
{
ssize_t vertices = m_Terrain->GetVerticesPerSide();
ENSURE(1 <= vertices && vertices <= 65535);
return (u16)vertices;
}
virtual CTerrain* GetCTerrain()
{
return m_Terrain;
}
virtual void ReloadTerrain(bool ReloadWater)
{
// TODO: should refactor this code to be nicer
u16 tiles = GetTilesPerSide();
u16 vertices = GetVerticesPerSide();
CmpPtr cmpObstructionManager(GetSystemEntity());
if (cmpObstructionManager)
{
cmpObstructionManager->SetBounds(entity_pos_t::Zero(), entity_pos_t::Zero(),
entity_pos_t::FromInt(tiles*(int)TERRAIN_TILE_SIZE),
entity_pos_t::FromInt(tiles*(int)TERRAIN_TILE_SIZE));
}
CmpPtr cmpRangeManager(GetSystemEntity());
if (cmpRangeManager)
{
cmpRangeManager->SetBounds(entity_pos_t::Zero(), entity_pos_t::Zero(),
entity_pos_t::FromInt(tiles*(int)TERRAIN_TILE_SIZE),
entity_pos_t::FromInt(tiles*(int)TERRAIN_TILE_SIZE),
vertices);
}
if (ReloadWater && CRenderer::IsInitialised())
{
g_Renderer.GetWaterManager()->SetMapSize(vertices);
g_Renderer.GetWaterManager()->RecomputeBlurredNormalMap();
g_Renderer.GetWaterManager()->RecomputeDistanceHeightmap();
g_Renderer.GetWaterManager()->RecomputeWindStrength();
g_Renderer.GetWaterManager()->CreateWaveMeshes();
}
MakeDirty(0, 0, tiles+1, tiles+1);
}
virtual void MakeDirty(i32 i0, i32 j0, i32 i1, i32 j1)
{
CMessageTerrainChanged msg(i0, j0, i1, j1);
GetSimContext().GetComponentManager().BroadcastMessage(msg);
}
};
REGISTER_COMPONENT_TYPE(Terrain)
Index: ps/trunk/source/simulation2/components/ICmpTerrain.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpTerrain.cpp (revision 19698)
+++ ps/trunk/source/simulation2/components/ICmpTerrain.cpp (revision 19699)
@@ -1,28 +1,29 @@
/* Copyright (C) 2017 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 "ICmpTerrain.h"
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(Terrain)
DEFINE_INTERFACE_METHOD_CONST_2("GetGroundLevel", entity_pos_t, ICmpTerrain, GetGroundLevel, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_CONST_2("CalcNormal", CFixedVector3D, ICmpTerrain, CalcNormal, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_CONST_0("GetTilesPerSide", u16, ICmpTerrain, GetTilesPerSide)
+DEFINE_INTERFACE_METHOD_CONST_0("GetMapSize", u32, ICmpTerrain, GetMapSize)
END_INTERFACE_WRAPPER(Terrain)
Index: ps/trunk/source/simulation2/components/ICmpTerrain.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpTerrain.h (revision 19698)
+++ ps/trunk/source/simulation2/components/ICmpTerrain.h (revision 19699)
@@ -1,74 +1,79 @@
/* Copyright (C) 2017 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_ICMPTERRAIN
#define INCLUDED_ICMPTERRAIN
#include "simulation2/system/Interface.h"
#include "simulation2/helpers/Position.h"
#include "maths/FixedVector3D.h"
class CTerrain;
class CVector3D;
class ICmpTerrain : public IComponent
{
public:
virtual bool IsLoaded() const = 0;
virtual CFixedVector3D CalcNormal(entity_pos_t x, entity_pos_t z) const = 0;
virtual CVector3D CalcExactNormal(float x, float z) const = 0;
virtual entity_pos_t GetGroundLevel(entity_pos_t x, entity_pos_t z) const = 0;
virtual float GetExactGroundLevel(float x, float z) const = 0;
/**
* Returns number of tiles per side on the terrain.
* Return value is always non-zero.
*/
virtual u16 GetTilesPerSide() const = 0;
/**
* Returns number of vertices per side on the terrain.
* Return value is always non-zero.
*/
virtual u16 GetVerticesPerSide() const = 0;
+ /**
+ * Returns the map size in metres (world space units).
+ */
+ virtual u32 GetMapSize() const = 0;
+
virtual CTerrain* GetCTerrain() = 0;
/**
* Call when the underlying CTerrain has been modified behind our backs.
* (TODO: eventually we should manage the CTerrain in this class so nobody
* can modify it behind our backs).
*/
virtual void ReloadTerrain(bool ReloadWater = true) = 0;
/**
* Indicate that terrain tiles within the given region (inclusive lower bound,
* exclusive upper bound) have been changed. CMessageTerrainChanged will be
* sent to any components that care about terrain changes.
*/
virtual void MakeDirty(i32 i0, i32 j0, i32 i1, i32 j1) = 0;
DECLARE_INTERFACE_TYPE(Terrain)
};
#endif // INCLUDED_ICMPTERRAIN
Index: ps/trunk/source/simulation2/system/ComponentTest.h
===================================================================
--- ps/trunk/source/simulation2/system/ComponentTest.h (revision 19698)
+++ ps/trunk/source/simulation2/system/ComponentTest.h (revision 19699)
@@ -1,232 +1,237 @@
/* Copyright (C) 2017 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 "lib/self_test.h"
#include "maths/Matrix3D.h"
#include "maths/Vector3D.h"
#include "ps/XML/Xeromyces.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/system/Component.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/serialization/DebugSerializer.h"
#include "simulation2/serialization/HashSerializer.h"
#include "simulation2/serialization/StdSerializer.h"
#include "simulation2/serialization/StdDeserializer.h"
#include
/**
* @file
* Various common features for component test cases.
*/
/**
* Class to test a single component.
* - Create an instance of this class
* - Use AddMock to add mock components that the tested component relies on
* - Use Add to add the test component itself, and it returns a component pointer
* - Call methods on the component pointer
* - Use Roundtrip to test the consistency of serialization
*/
class ComponentTestHelper
{
CSimContext m_Context;
CComponentManager m_ComponentManager;
CParamNode m_Param;
IComponent* m_Cmp;
EComponentTypeId m_Cid;
public:
ComponentTestHelper(shared_ptr runtime) :
m_Context(), m_ComponentManager(m_Context, runtime), m_Cmp(NULL)
{
m_ComponentManager.LoadComponentTypes();
}
ScriptInterface& GetScriptInterface()
{
return m_ComponentManager.GetScriptInterface();
}
CSimContext& GetSimContext()
{
return m_Context;
}
/**
* Call this once to initialise the test helper with a component.
*/
template
T* Add(EComponentTypeId cid, const std::string& xml, entity_id_t ent = 10)
{
TS_ASSERT(m_Cmp == NULL);
CEntityHandle handle;
if (ent == SYSTEM_ENTITY)
{
m_ComponentManager.InitSystemEntity();
handle = m_ComponentManager.GetSystemEntity();
m_Context.SetSystemEntity(handle);
}
else
handle = m_ComponentManager.LookupEntityHandle(ent, true);
m_Cid = cid;
TS_ASSERT_EQUALS(CParamNode::LoadXMLString(m_Param, ("" + xml + "").c_str()), PSRETURN_OK);
TS_ASSERT(m_ComponentManager.AddComponent(handle, m_Cid, m_Param.GetChild("test")));
m_Cmp = m_ComponentManager.QueryInterface(ent, T::GetInterfaceId());
TS_ASSERT(m_Cmp != NULL);
return static_cast (m_Cmp);
}
void AddMock(entity_id_t ent, EInterfaceId iid, IComponent& component)
{
CEntityHandle handle;
if (ent == SYSTEM_ENTITY)
{
m_ComponentManager.InitSystemEntity();
handle = m_ComponentManager.GetSystemEntity();
m_Context.SetSystemEntity(handle);
}
else
handle = m_ComponentManager.LookupEntityHandle(ent, true);
m_ComponentManager.AddMockComponent(handle, iid, component);
}
void HandleMessage(IComponent* cmp, const CMessage& msg, bool global)
{
cmp->HandleMessage(msg, global);
}
/**
* Checks that the object roundtrips through its serialize/deserialize functions correctly.
* Computes the debug output, hash, and binary serialization; then deserializes into a new
* system and checks the serialization outputs are unchanged.
*/
void Roundtrip(bool verbose = false)
{
std::stringstream dbgstr1;
CDebugSerializer dbg1(GetScriptInterface(), dbgstr1);
m_Cmp->Serialize(dbg1);
if (verbose)
std::cout << "--------\n" << dbgstr1.str() << "--------\n";
CHashSerializer hash1(GetScriptInterface());
m_Cmp->Serialize(hash1);
std::stringstream stdstr1;
CStdSerializer std1(GetScriptInterface(), stdstr1);
m_Cmp->Serialize(std1);
ComponentTestHelper test2(GetScriptInterface().GetRuntime());
// (We should never need to add any mock objects etc to test2, since deserialization
// mustn't depend on other components already existing)
CEntityHandle ent = test2.m_ComponentManager.LookupEntityHandle(10, true);
CStdDeserializer stdde2(test2.GetScriptInterface(), stdstr1);
IComponent* cmp2 = test2.m_ComponentManager.ConstructComponent(ent, m_Cid);
cmp2->Deserialize(m_Param.GetChild("test"), stdde2);
TS_ASSERT(stdstr1.peek() == EOF); // Deserialize must read whole stream
std::stringstream dbgstr2;
CDebugSerializer dbg2(test2.GetScriptInterface(), dbgstr2);
cmp2->Serialize(dbg2);
if (verbose)
std::cout << "--------\n" << dbgstr2.str() << "--------\n";
CHashSerializer hash2(test2.GetScriptInterface());
cmp2->Serialize(hash2);
std::stringstream stdstr2;
CStdSerializer std2(test2.GetScriptInterface(), stdstr2);
cmp2->Serialize(std2);
TS_ASSERT_EQUALS(dbgstr1.str(), dbgstr2.str());
TS_ASSERT_EQUALS(hash1.GetHashLength(), hash2.GetHashLength());
TS_ASSERT_SAME_DATA(hash1.ComputeHash(), hash2.ComputeHash(), hash1.GetHashLength());
TS_ASSERT_EQUALS(stdstr1.str(), stdstr2.str());
// TODO: need to extend this so callers can run methods on the cloned component
// to check that all its data is still correct
}
};
/**
* Simple terrain implementation with constant height of 50.
*/
class MockTerrain : public ICmpTerrain
{
public:
DEFAULT_MOCK_COMPONENT()
virtual bool IsLoaded() const
{
return true;
}
virtual CFixedVector3D CalcNormal(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) const
{
return CFixedVector3D(fixed::FromInt(0), fixed::FromInt(1), fixed::FromInt(0));
}
virtual CVector3D CalcExactNormal(float UNUSED(x), float UNUSED(z)) const
{
return CVector3D(0.f, 1.f, 0.f);
}
virtual entity_pos_t GetGroundLevel(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) const
{
return entity_pos_t::FromInt(50);
}
virtual float GetExactGroundLevel(float UNUSED(x), float UNUSED(z)) const
{
return 50.f;
}
virtual u16 GetTilesPerSide() const
{
return 16;
}
+ virtual u32 GetMapSize() const
+ {
+ return GetTilesPerSide() * TERRAIN_TILE_SIZE;
+ }
+
virtual u16 GetVerticesPerSide() const
{
return 17;
}
virtual CTerrain* GetCTerrain()
{
return NULL;
}
virtual void MakeDirty(i32 UNUSED(i0), i32 UNUSED(j0), i32 UNUSED(i1), i32 UNUSED(j1))
{
}
virtual void ReloadTerrain(bool UNUSED(ReloadWater))
{
}
};