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)) { } };