Index: ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_spec.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_spec.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_spec.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_spec.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_spec.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js (revision 25690) +++ ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js (revision 25691) @@ -1,17 +1,23 @@ /** * This class is concerned with managing the different elements of the minimap panel. */ class MiniMapPanel { constructor(playerViewControl, diplomacyColors, idleWorkerClasses) { this.diplomacyColorsButton = new MiniMapDiplomacyColorsButton(diplomacyColors); this.idleWorkerButton = new MiniMapIdleWorkerButton(playerViewControl, idleWorkerClasses); + this.flareButton = new MiniMapFlareButton(); this.miniMap = new MiniMap(); } + flare(target, playerID) + { + return this.miniMap.flare(target, playerID); + } + isMouseOverMiniMap() { return this.miniMap.isMouseOverMiniMap(); } } Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25690) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25691) @@ -1,1836 +1,1846 @@ // Setting this to true will display some warnings when commands // are likely to fail, which may be useful for debugging AIs var g_DebugCommands = false; function ProcessCommand(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; let data = { "cmpPlayer": cmpPlayer, "controlAllUnits": cmpPlayer.CanControlAllUnits() }; if (cmd.entities) data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits); // TODO: queuing order and forcing formations doesn't really work. // To play nice, we'll still no-formation queued order if units are in formation // but the opposite perhaps ought to be implemented. if (!cmd.queued || cmd.formation == NULL_FORMATION) data.formation = cmd.formation || undefined; // Allow focusing the camera on recent commands let commandData = { "type": "playercommand", "players": [player], "cmd": cmd }; // Save the position, since the GUI event is received after the unit died if (cmd.type == "delete-entities") { let cmpPosition = cmd.entities[0] && Engine.QueryInterface(cmd.entities[0], IID_Position); commandData.position = cmpPosition && cmpPosition.IsInWorld() && cmpPosition.GetPosition2D(); } let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification(commandData); // Note: checks of UnitAI targets are not robust enough here, as ownership // can change after the order is issued, they should be checked by UnitAI // when the specific behavior (e.g. attack, garrison) is performed. // (Also it's not ideal if a command silently fails, it's nicer if UnitAI // moves the entities closer to the target before giving up.) // Now handle various commands if (g_Commands[cmd.type]) { var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("OnPlayerCommand", { "player": player, "cmd": cmd }); g_Commands[cmd.type](player, cmd, data); } else error("Invalid command: unknown command type: "+uneval(cmd)); } var g_Commands = { "aichat": function(player, cmd, data) { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); var notification = { "players": [player] }; for (var key in cmd) notification[key] = cmd[key]; cmpGuiInterface.PushNotification(notification); }, "cheat": function(player, cmd, data) { Cheat(cmd); }, "collect-treasure": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CollectTreasure(cmd.target, cmd.queued); }); }, "collect-treasure-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CollectTreasureNearPosition(cmd.x, cmd.z, cmd.queued); }); }, "diplomacy": function(player, cmd, data) { let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (data.cmpPlayer.GetLockTeams() || cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive()) return; switch(cmd.to) { case "ally": data.cmpPlayer.SetAlly(cmd.player); break; case "neutral": data.cmpPlayer.SetNeutral(cmd.player); break; case "enemy": data.cmpPlayer.SetEnemy(cmd.player); break; default: warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "diplomacy", "players": [player], "targetPlayer": cmd.player, "status": cmd.to }); }, "tribute": function(player, cmd, data) { data.cmpPlayer.TributeResource(cmd.player, cmd.amounts); }, "control-all": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - control all units)") }); data.cmpPlayer.SetControlAllUnits(cmd.flag); }, "reveal-map": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - reveal map)") }); // Reveal the map for all players, not just the current player, // primarily to make it obvious to everyone that the player is cheating var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetLosRevealAll(-1, cmd.enable); }, "walk": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront); }); }, "walk-custom": function(player, cmd, data) { for (let ent in data.entities) GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued, cmd.pushFront); }); }, "walk-to-range": function(player, cmd, data) { // Only used by the AI for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued, cmd.pushFront); } }, "attack-walk": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront); }); }, "attack-walk-custom": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; for (let ent in data.entities) GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront); }); }, "attack": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; if (g_DebugCommands && !allowCapture && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Attack(cmd.target, allowCapture, cmd.queued, cmd.pushFront); }); }, "patrol": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued) ); }, "heal": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Heal(cmd.target, cmd.queued, cmd.pushFront); }); }, "repair": function(player, cmd, data) { // This covers both repairing damaged buildings, and constructing unfinished foundations if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued, cmd.pushFront); }); }, "gather": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Gather(cmd.target, cmd.queued, cmd.pushFront); }); }, "gather-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued, cmd.pushFront); }); }, "returnresource": function(player, cmd, data) { if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.ReturnResource(cmd.target, cmd.queued, cmd.pushFront); }); }, "back-to-work": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(!cmpUnitAI || !cmpUnitAI.BackToWork()) notifyBackToWorkFailure(player); } }, "remove-guard": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.RemoveGuard(); } }, "train": function(player, cmd, data) { if (!Number.isInteger(cmd.count) || cmd.count <= 0) { warn("Invalid command: can't train " + uneval(cmd.count) + " units"); return; } // Check entity limits var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template); var unitCategory = null; if (template.TrainingRestrictions) unitCategory = template.TrainingRestrictions.Category; // Verify that the building(s) can be controlled by the player if (data.entities.length <= 0) { if (g_DebugCommands) warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd)); return; } for (let ent of data.entities) { if (unitCategory) { var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits); if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit)) { if (g_DebugCommands) warn(unitCategory + " train limit is reached: " + uneval(cmd)); continue; } } var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: training requires unresearched technology: " + uneval(cmd)); continue; } var queue = Engine.QueryInterface(ent, IID_ProductionQueue); // Check if the building can train the unit // TODO: the AI API does not take promotion technologies into account for the list // of trainable units (taken directly from the unit template). Here is a temporary fix. if (queue && data.cmpPlayer.IsAI()) { var list = queue.GetEntitiesList(); if (list.indexOf(cmd.template) === -1 && cmd.promoted) { for (var promoted of cmd.promoted) { if (list.indexOf(promoted) === -1) continue; cmd.template = promoted; break; } } } if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1) if ("metadata" in cmd) queue.AddItem(cmd.template, "unit", +cmd.count, cmd.metadata); else queue.AddItem(cmd.template, "unit", +cmd.count); } }, "research": function(player, cmd, data) { var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanResearch(cmd.template)) { if (g_DebugCommands) warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd)); return; } var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (queue) queue.AddItem(cmd.template, "technology"); }, "stop-production": function(player, cmd, data) { let cmpProductionQueue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.RemoveItem(cmd.id); }, "construct": function(player, cmd, data) { TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "construct-wall": function(player, cmd, data) { TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "delete-entities": function(player, cmd, data) { for (let ent of data.entities) { if (!data.controlAllUnits) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity && cmpIdentity.IsUndeletable()) continue; let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable && cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2) continue; let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather()) continue; } let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) { let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health); if (cmpMiragedHealth) cmpMiragedHealth.Kill(); else Engine.DestroyEntity(cmpMirage.parent); Engine.DestroyEntity(ent); continue; } let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) cmpHealth.Kill(); else Engine.DestroyEntity(ent); } }, "set-rallypoint": function(player, cmd, data) { for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { if (!cmd.queued) cmpRallyPoint.Unset(); cmpRallyPoint.AddPosition(cmd.x, cmd.z); cmpRallyPoint.AddData(clone(cmd.data)); } } }, "unset-rallypoint": function(player, cmd, data) { for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.Reset(); } }, "resign": function(player, cmd, data) { data.cmpPlayer.SetState("defeated", markForTranslation("%(player)s has resigned.")); }, "occupy-turret": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.OccupyTurret(cmd.target, cmd.queued); }); }, "garrison": function(player, cmd, data) { if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Garrison(cmd.target, cmd.queued, cmd.pushFront); }); }, "guard": function(player, cmd, data) { if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Guard(cmd.target, cmd.queued, cmd.pushFront); }); }, "stop": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Stop(cmd.queued); }); }, "leave-turret": function(player, cmd, data) { let notUnloaded = 0; for (let ent of data.entities) { let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (!cmpTurretable || !cmpTurretable.LeaveTurret()) ++notUnloaded; } if (notUnloaded) notifyUnloadFailure(player); }, "unload-turrets": function(player, cmd, data) { let notUnloaded = 0; for (let ent of data.entities) { let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); for (let turret of cmpTurretHolder.GetEntities()) { let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable); if (!cmpTurretable || !cmpTurretable.LeaveTurret()) ++notUnloaded; } } if (notUnloaded) notifyUnloadFailure(player); }, "unload": function(player, cmd, data) { if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); var notUngarrisoned = 0; // The owner can ungarrison every garrisoned unit if (IsOwnedByPlayer(player, cmd.garrisonHolder)) data.entities = cmd.entities; for (let ent of data.entities) if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent)) ++notUngarrisoned; if (notUngarrisoned != 0) notifyUnloadFailure(player, cmd.garrisonHolder); }, "unload-template": function(player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (cmpGarrisonHolder) { // Only the owner of the garrisonHolder may unload entities from any owners if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits && player != +cmd.owner) continue; if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all)) notifyUnloadFailure(player, garrisonHolder); } } }, "unload-all-by-owner": function(player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player)) notifyUnloadFailure(player, garrisonHolder); } }, "unload-all": function(player, cmd, data) { var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll()) notifyUnloadFailure(player, garrisonHolder); } }, "alert-raise": function(player, cmd, data) { for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.RaiseAlert(); } }, "alert-end": function(player, cmd, data) { for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.EndOfAlert(); } }, "formation": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => { cmpUnitAI.MoveIntoFormation(cmd); }); }, "promote": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - promoted units)"), "translateMessage": true }); for (let ent of cmd.entities) { var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp()); } }, "stance": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && !cmpUnitAI.IsTurret()) cmpUnitAI.SwitchToStance(cmd.name); } }, "lock-gate": function(player, cmd, data) { for (let ent of data.entities) { var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (!cmpGate) continue; if (cmd.lock) cmpGate.LockGate(); else cmpGate.UnlockGate(); } }, "setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); }); }, "cancel-setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CancelSetupTradeRoute(cmd.target); }); }, "set-trading-goods": function(player, cmd, data) { data.cmpPlayer.SetTradingGoods(cmd.tradingGoods); }, "barter": function(player, cmd, data) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); cmpBarter.ExchangeResources(player, cmd.sell, cmd.buy, cmd.amount); }, "set-shading-color": function(player, cmd, data) { // Prevent multiplayer abuse if (!data.cmpPlayer.IsAI()) return; // Debug command to make an entity brightly colored for (let ent of cmd.entities) { var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0 } }, "pack": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; if (cmd.pack) cmpUnitAI.Pack(cmd.queued, cmd.pushFront); else cmpUnitAI.Unpack(cmd.queued, cmd.pushFront); } }, "cancel-pack": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; if (cmd.pack) cmpUnitAI.CancelPack(cmd.queued, cmd.pushFront); else cmpUnitAI.CancelUnpack(cmd.queued, cmd.pushFront); } }, "upgrade": function(player, cmd, data) { for (let ent of data.entities) { var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template)) continue; if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template)) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [player], "message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.") }); continue; } // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (cmpEntityLimits && !cmpEntityLimits.AllowedToReplace(ent, cmd.template)) { if (g_DebugCommands) warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd)); continue; } let cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); let requiredTechnology = cmpUpgrade.GetRequiredTechnology(cmd.template); if (requiredTechnology && (!cmpTechnologyManager || !cmpTechnologyManager.IsTechnologyResearched(requiredTechnology))) { if (g_DebugCommands) warn("Invalid command: upgrading is not possible for this player or requires unresearched technology: " + uneval(cmd)); continue; } cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer); } }, "cancel-upgrade": function(player, cmd, data) { for (let ent of data.entities) { let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) cmpUpgrade.CancelUpgrade(player); } }, "attack-request": function(player, cmd, data) { // Send a chat message to human players var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": "/allies " + markForTranslation("Attack against %(_player_)s requested."), "translateParameters": ["_player_"], "parameters": { "_player_": cmd.player } }); // And send an attackRequest event to the AIs let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("AttackRequest", cmd); }, "spy-request": function(player, cmd, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => { let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing); return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player); })); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "spy-response", "players": [player], "target": cmd.player, "entity": ent }); if (ent) Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source); else { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy"); IncurBribeCost(template, player, cmd.player, true); // update statistics for failed bribes let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpBribesStatisticsTracker) cmpBribesStatisticsTracker.IncreaseFailedBribesCounter(); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("There are no bribable units"), "translateMessage": true }); } }, "diplomacy-request": function(player, cmd, data) { let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("DiplomacyRequest", cmd); }, "tribute-request": function(player, cmd, data) { let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("TributeRequest", cmd); }, "dialog-answer": function(player, cmd, data) { // Currently nothing. Triggers can read it anyway, and send this // message to any component you like. }, "set-dropsite-sharing": function(player, cmd, data) { for (let ent of data.entities) { let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite && cmpResourceDropsite.IsSharable()) cmpResourceDropsite.SetSharing(cmd.shared); } }, + "map-flare": function(player, cmd, data) + { + let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({ + "type": "map-flare", + "players": [player], + "target": cmd.target + }); + }, + "autoqueue-on": function(player, cmd, data) { for (let ent of data.entities) { let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.EnableAutoQueue(); } }, "autoqueue-off": function(player, cmd, data) { for (let ent of data.entities) { let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.DisableAutoQueue(); } }, }; /** * Sends a GUI notification about unit(s) that failed to ungarrison. */ function notifyUnloadFailure(player) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("Unable to unload unit(s)."), "translateMessage": true }); } /** * Sends a GUI notification about worker(s) that failed to go back to work. */ function notifyBackToWorkFailure(player) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("Some unit(s) can't go back to work"), "translateMessage": true }); } /** * Sends a GUI notification about entities that can't be controlled. * @param {number} player - The player-ID of the player that needs to receive this message. */ function notifyOrderFailure(entity, player) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); if (!cmpIdentity) return; let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": sprintf(markForTranslation("%(unit)s can't be controlled."), { "unit": cmpIdentity.GetGenericName() }), "translateMessage": true }); } /** * Get some information about the formations used by entities. */ function ExtractFormations(ents) { let entities = []; // Entities with UnitAI. let members = {}; // { formationentity: [ent, ent, ...], ... } let templates = {}; // { formationentity: template } for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; entities.push(ent); let fid = cmpUnitAI.GetFormationController(); if (fid == INVALID_ENTITY) continue; if (!members[fid]) { members[fid] = []; templates[fid] = cmpUnitAI.GetFormationTemplate(); } members[fid].push(ent); } return { "entities": entities, "members": members, "templates": templates }; } /** * Tries to find the best angle to put a dock at a given position * Taken from GuiInterface.js */ function GetDockAngle(template, x, z) { var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); if (!cmpTerrain || !cmpWaterManager) return undefined; // Get footprint size var halfSize = 0; if (template.Footprint.Square) halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; else if (template.Footprint.Circle) halfSize = template.Footprint.Circle["@radius"]; /* Find direction of most open water, algorithm: * 1. Pick points in a circle around dock * 2. If point is in water, add to array * 3. Scan array looking for consecutive points * 4. Find longest sequence of consecutive points * 5. If sequence equals all points, no direction can be determined, * expand search outward and try (1) again * 6. Calculate angle using average of sequence */ const numPoints = 16; for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; for (var i = 0; i < numPoints; ++i) { var angle = (i/numPoints)*2*Math.PI; var d = halfSize*(dist+1); var nx = x - d*Math.sin(angle); var nz = z + d*Math.cos(angle); if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) waterPoints.push(i); } var consec = []; var length = waterPoints.length; if (!length) continue; for (var i = 0; i < length; ++i) { var count = 0; for (let j = 0; j < length - 1; ++j) { if ((waterPoints[(i + j) % length] + 1) % numPoints == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } var start = 0; var count = 0; for (var c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -((waterPoints[start] + consec[start]/2) % numPoints) / numPoints * 2 * Math.PI; } return undefined; } /** * Attempts to construct a building using the specified parameters. * Returns true on success, false on failure. */ function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd) { // Message structure: // { // "type": "construct", // "entities": [...], // entities that will be ordered to construct the building (if applicable) // "template": "...", // template name of the entity being constructed // "x": ..., // "z": ..., // "angle": ..., // "metadata": "...", // AI metadata of the building // "actorSeed": ..., // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable) // "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. If specified, must be a valid control group ID (> 0). // "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. May be INVALID_ENTITY. // } /* * Construction process: * . Take resources away immediately. * . Create a foundation entity with 1hp, 0% build progress. * . Increase hp and build progress up to 100% when people work on it. * . If it's destroyed, an appropriate fraction of the resource cost is refunded. * . If it's completed, it gets replaced with the real building. */ // Check whether we can control these units var entities = FilterEntityList(cmd.entities, player, controlAllUnits); if (!entities.length) return false; var foundationTemplate = "foundation|" + cmd.template; // Tentatively create the foundation (we might find later that it's a invalid build command) var ent = Engine.AddEntity(foundationTemplate); if (ent == INVALID_ENTITY) { // Error (e.g. invalid template names) error("Error creating foundation entity for '" + cmd.template + "'"); return false; } // If it's a dock, get the right angle. var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template); var angle = cmd.angle; if (template.BuildRestrictions.PlacementType === "shore") { let angleDock = GetDockAngle(template, cmd.x, cmd.z); if (angleDock !== undefined) angle = angleDock; } // Move the foundation to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); cmpPosition.SetYRotation(angle); // Set the obstruction control group if needed if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2) { var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); // primary control group must always be valid if (cmd.obstructionControlGroup) { if (cmd.obstructionControlGroup <= 0) warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0"); cmpObstruction.SetControlGroup(cmd.obstructionControlGroup); } if (cmd.obstructionControlGroup2) cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2); } // Make it owned by the current player var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (cmpBuildRestrictions) { var ret = cmpBuildRestrictions.CheckPlacement(); if (!ret.success) { if (g_DebugCommands) warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd)); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); ret.players = [player]; cmpGuiInterface.PushNotification(ret); // Remove the foundation because the construction was aborted // move it out of world because it's not destroyed immediately. cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } } else error("cmpBuildRestrictions not defined"); // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) { if (g_DebugCommands) warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd)); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd)); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("The building's technology requirements are not met."), "translateMessage": true }); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); } // We need the cost after tech and aura modifications. let cmpCost = Engine.QueryInterface(ent, IID_Cost); let costs = cmpCost.GetResourceCosts(); if (!cmpPlayer.TrySubtractResources(costs)) { if (g_DebugCommands) warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd)); Engine.DestroyEntity(ent); cmpPosition.MoveOutOfWorld(); return false; } var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual && cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); // Initialise the foundation var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); cmpFoundation.InitialiseConstruction(cmd.template); // send Metadata info if any if (cmd.metadata) Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } ); // Tell the units to start building this new entity if (cmd.autorepair) { ProcessCommand(player, { "type": "repair", "entities": entities, "target": ent, "autocontinue": cmd.autocontinue, "queued": cmd.queued, "pushFront": cmd.pushFront, "formation": cmd.formation || undefined }); } return ent; } function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd) { // 'cmd' message structure: // { // "type": "construct-wall", // "entities": [...], // entities that will be ordered to construct the wall (if applicable) // "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...) // { // "template": "...", // one of the templates from the wallset // "x": ..., // "z": ..., // "angle": ..., // }, // ... // ], // "wallSet": { // "templates": { // "tower": // tower template name // "long": // long wall segment template name // ... // etc. // }, // "maxTowerOverlap": ..., // "minTowerOverlap": ..., // }, // "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall // "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable) // } if (cmd.pieces.length <= 0) return; if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side"); return; } if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side"); return; } // Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement // and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control // groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing // towers in the case of snapping). The towers themselves all keep their default unique control groups. // To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour // it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the // first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of // the first tower encountered towards the ending side of the wall (if any). // We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the // wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes: // // FIRST PASS: // - Go from start to end and construct wall piece foundations as far as we can without running into a piece that // cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID // as the primary control group, thus allowing it to be built overlapping the previous piece. // - If we encounter a new tower along the way (which will gain its own control group), do the following: // o First build it using temporarily the same control group of the previous (non-tower) piece // o Set the previous piece's secondary control group to the tower's entity ID // o Restore the primary control group of the constructed tower back its original (unique) value. // The temporary control group is necessary to allow the newer tower with its unique control group ID to be able // to be placed while overlapping the previous piece. // // SECOND PASS: // - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this // time registering the right neighbouring tower in each non-tower piece. // first pass; L -> R var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that // the first wall piece can be built while overlapping it. if (cmd.startSnappedEntity) { var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction); if (!cmpSnappedStartObstruction) { error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup(); //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup); } var i = 0; var queued = cmd.queued; var pieces = clone(cmd.pieces); for (; i < pieces.length; ++i) { var piece = pieces[i]; // All wall pieces after the first must be queued. if (i > 0 && !queued) queued = true; // 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do // start position snapping (implying that the first entity we build must be a tower) if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY) { if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity)) { error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")"); break; } } var constructPieceCmd = { "type": "construct", "entities": cmd.entities, "template": piece.template, "x": piece.x, "z": piece.z, "angle": piece.angle, "autorepair": cmd.autorepair, "autocontinue": cmd.autocontinue, "queued": queued, // Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed // using the control group of the last tower (see comments above). "obstructionControlGroup": lastTowerControlGroup, }; // If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's // control group directly at construction time (instead of setting it in the second pass) to allow it to be built // while overlapping the snapped entity. if (i == pieces.length - 1 && cmd.endSnappedEntity) { var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (cmpEndSnappedObstruction) constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup(); } var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd); if (pieceEntityId) { // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later piece.ent = pieceEntityId; // if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex if (piece.template == cmd.wallSet.templates.tower) { var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction); var newTowerControlGroup = pieceEntityId; if (i > 0) { //warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup); var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction); // TODO: ensure that cmpPreviousObstruction exists // TODO: ensure that the previous obstruction does not yet have a secondary control group set cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup); } // TODO: ensure that cmpTowerObstruction exists cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group lastTowerIndex = i; lastTowerControlGroup = newTowerControlGroup; } } else // failed to build wall piece, abort break; } var lastBuiltPieceIndex = i - 1; var wallComplete = (lastBuiltPieceIndex == pieces.length - 1); // At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower). // Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any) // as their secondary control groups. lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // only start off with the ending side's snapped tower's control group if we were able to build the entire wall if (cmd.endSnappedEntity && wallComplete) { var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (!cmpSnappedEndObstruction) { error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup(); } for (var j = lastBuiltPieceIndex; j >= 0; --j) { var piece = pieces[j]; if (!piece.ent) { error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'"); continue; } var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction); if (!cmpPieceObstruction) { error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component"); continue; } if (piece.template == cmd.wallSet.templates.tower) { // encountered a tower entity, update the last tower control group lastTowerControlGroup = cmpPieceObstruction.GetControlGroup(); } else { // Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'. // Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group // dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'. var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2(); if (existingSecondaryControlGroup == INVALID_ENTITY) { if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY) { cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup); } } else if (existingSecondaryControlGroup != lastTowerControlGroup) { error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")"); break; } } } } /** * Remove the given list of entities from their current formations. */ function RemoveFromFormation(ents) { let formation = ExtractFormations(ents); for (let fid in formation.members) { let cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } } /** * Returns a list of UnitAI components, each belonging either to a * selected unit or to a formation entity for groups of the selected units. */ function GetFormationUnitAIs(ents, player, cmd, formationTemplate, forceTemplate) { // If an individual was selected, remove it from any formation // and command it individually. if (ents.length == 1) { let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); if (!cmpUnitAI) return []; RemoveFromFormation(ents); return [ cmpUnitAI ]; } let formationUnitAIs = []; // Find what formations the selected entities are currently in, // and default to that unless the formation is forced or it's the null formation // (we want that to reset whatever formations units are in). if (formationTemplate != NULL_FORMATION) { let formation = ExtractFormations(ents); let formationIds = Object.keys(formation.members); if (formationIds.length == 1) { // Selected units either belong to this formation or have no formation. let fid = formationIds[0]; let cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length && cmpFormation.GetMemberCount() == formation.entities.length) { cmpFormation.DeleteTwinFormations(); // The whole formation was selected, so reuse its controller for this command. if (!forceTemplate || formationTemplate == formation.templates[fid]) { formationTemplate = formation.templates[fid]; formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; } else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate)) formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)]; } else if (cmpFormation && !forceTemplate) { // Just reuse the template. formationTemplate = formation.templates[fid]; } } else if (formationIds.length) { // Check if all entities share a common formation, if so reuse this template. let template = formation.templates[formationIds[0]]; for (let i = 1; i < formationIds.length; ++i) if (formation.templates[formationIds[i]] != template) { template = null; break; } if (template && !forceTemplate) formationTemplate = template; } } // Separate out the units that don't support the chosen formation. let formedUnits = []; let nonformedUnitAIs = []; for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld()) continue; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: We only check if the formation is usable by some units // if we move them to it. We should check if we can use formations // for the other cases. let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION; if (nullFormation || !cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate || NULL_FORMATION)) { if (nullFormation && cmpUnitAI.GetFormationController()) cmpUnitAI.LeaveFormation(cmd.queued || false); nonformedUnitAIs.push(cmpUnitAI); } else formedUnits.push(ent); } if (nonformedUnitAIs.length == ents.length) { // No units support the formation. return nonformedUnitAIs; } if (!formationUnitAIs.length) { // We need to give the selected units a new formation controller. // TODO replace the fixed 60 with something sensible, based on vision range f.e. let formationSeparation = 60; let clusters = ClusterEntities(formedUnits, formationSeparation); let formationEnts = []; for (let cluster of clusters) { RemoveFromFormation(cluster); if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) { for (let ent of cluster) nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI)); continue; } // Create the new controller. let formationEnt = Engine.AddEntity(formationTemplate); let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI)); cmpFormation.SetFormationSeparation(formationSeparation); cmpFormation.SetMembers(cluster); for (let ent of formationEnts) cmpFormation.RegisterTwinFormation(ent); formationEnts.push(formationEnt); let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership); cmpOwnership.SetOwner(player); } } return nonformedUnitAIs.concat(formationUnitAIs); } /** * Group a list of entities in clusters via single-links */ function ClusterEntities(ents, separationDistance) { let clusters = []; if (!ents.length) return clusters; let distSq = separationDistance * separationDistance; let positions = []; // triangular matrix with the (squared) distances between the different clusters // the other half is not initialised let matrix = []; for (let i = 0; i < ents.length; ++i) { matrix[i] = []; clusters.push([ents[i]]); let cmpPosition = Engine.QueryInterface(ents[i], IID_Position); positions.push(cmpPosition.GetPosition2D()); for (let j = 0; j < i; ++j) matrix[i][j] = positions[i].distanceToSquared(positions[j]); } while (clusters.length > 1) { // search two clusters that are closer than the required distance let closeClusters = undefined; for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i) for (let j = i - 1; j >= 0 && !closeClusters; --j) if (matrix[i][j] < distSq) closeClusters = [i,j]; // if no more close clusters found, just return all found clusters so far if (!closeClusters) return clusters; // make a new cluster with the entities from the two found clusters let newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]); // calculate the minimum distance between the new cluster and all other remaining // clusters by taking the minimum of the two distances. let distances = []; for (let i = 0; i < clusters.length; ++i) { let a = closeClusters[1]; let b = closeClusters[0]; if (i == a || i == b) continue; let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a]; let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b]; distances.push(Math.min(dist1, dist2)); } // remove the rows and columns in the matrix for the merged clusters, // and the clusters themselves from the cluster list clusters.splice(closeClusters[0],1); clusters.splice(closeClusters[1],1); matrix.splice(closeClusters[0],1); matrix.splice(closeClusters[1],1); for (let i = 0; i < matrix.length; ++i) { if (matrix[i].length > closeClusters[0]) matrix[i].splice(closeClusters[0],1); if (matrix[i].length > closeClusters[1]) matrix[i].splice(closeClusters[1],1); } // add a new row of distances to the matrix and the new cluster clusters.push(newCluster); matrix.push(distances); } return clusters; } function GetFormationRequirements(formationTemplate) { var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate); if (!template.Formation) return false; return { "minCount": +template.Formation.RequiredMemberCount }; } function CanMoveEntsIntoFormation(ents, formationTemplate) { // TODO: should check the player's civ is allowed to use this formation // See simulation/components/Player.js GetFormations() for a list of all allowed formations var requirements = GetFormationRequirements(formationTemplate); if (!requirements) return false; var count = 0; for (let ent of ents) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate)) continue; ++count; } return count >= requirements.minCount; } /** * Check if player can control this entity * returns: true if the entity is owned by the player and controllable * or control all units is activated, else false */ function CanControlUnit(entity, player, controlAll) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); let canBeControlled = IsOwnedByPlayer(player, entity) && (!cmpIdentity || cmpIdentity.IsControllable()) || controlAll; if (!canBeControlled) notifyOrderFailure(entity, player); return canBeControlled; } /** * @param {number} entity - The entityID to verify. * @param {number} player - The playerID to check against. * @return {boolean}. */ function IsOwnedByPlayerOrMutualAlly(entity, player) { return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity); } /** * Check if player can control this entity * @return {boolean} - True if the entity is valid and controlled by the player * or the entity is owned by an mutualAlly and can be controlled * or control all units is activated, else false. */ function CanPlayerOrAllyControlUnit(entity, player, controlAll) { return CanControlUnit(player, entity, controlAll) || IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity); } /** * @return {boolean} - Whether the owner of this entity can control the entity. */ function CanOwnerControlEntity(entity) { let cmpOwner = QueryOwnerInterface(entity); return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID()); } /** * Filter entities which the player can control. */ function FilterEntityList(entities, player, controlAll) { return entities.filter(ent => CanControlUnit(ent, player, controlAll)); } /** * Filter entities which the player can control or are mutualAlly */ function FilterEntityListWithAllies(entities, player, controlAll) { return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll)); } /** * Incur the player with the cost of a bribe, optionally multiply the cost with * the additionalMultiplier */ function IncurBribeCost(template, player, playerBribed, failedBribe) { let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed); if (!cmpPlayerBribed) return false; let costs = {}; // Additional cost for this owner let multiplier = cmpPlayerBribed.GetSpyCostMultiplier(); if (failedBribe) multiplier *= template.VisionSharing.FailureCostRatio; for (let res in template.Cost.Resources) costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template)); let cmpPlayer = QueryPlayerIDInterface(player); return cmpPlayer && cmpPlayer.TrySubtractResources(costs); } Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements); Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation); Engine.RegisterGlobal("GetDockAngle", GetDockAngle); Engine.RegisterGlobal("ProcessCommand", ProcessCommand); Engine.RegisterGlobal("g_Commands", g_Commands); Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost); Index: ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 25690) +++ ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 25691) @@ -1,645 +1,727 @@ /* Copyright (C) 2021 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 "CMiniMap.h" #include "graphics/Canvas2D.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MiniMapTexture.h" #include "graphics/MiniPatch.h" #include "graphics/ShaderManager.h" #include "graphics/ShaderProgramPtr.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/GUIMatrix.h" #include "lib/bits.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "lib/timer.h" +#include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/WaterManager.h" #include "scriptinterface/Object.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/helpers/Los.h" #include "simulation2/system/ParamNode.h" #include #include #include extern bool g_GameRestarted; namespace { // Set max drawn entities to UINT16_MAX for now, which is more than enough // TODO: we should be cleverer about drawing them to reduce clutter const u16 MAX_ENTITIES_DRAWN = 65535; // Adds segments pieces lying inside the circle to lines. void CropPointsByCircle(const std::array& points, const CVector3D& center, const float radius, std::vector* lines) { constexpr float EPS = 1e-3f; lines->reserve(points.size() * 2); for (size_t idx = 0; idx < points.size(); ++idx) { const CVector3D& currentPoint = points[idx]; const CVector3D& nextPoint = points[(idx + 1) % points.size()]; const CVector3D direction = (nextPoint - currentPoint).Normalized(); const CVector3D normal(direction.Z, 0.0f, -direction.X); const float offset = normal.Dot(currentPoint) - normal.Dot(center); // We need to have lines only inside the circle. if (std::abs(offset) + EPS >= radius) continue; const CVector3D closestPoint = center + normal * offset; const float halfChordLength = sqrt(radius * radius - offset * offset); const CVector3D intersectionA = closestPoint - direction * halfChordLength; const CVector3D intersectionB = closestPoint + direction * halfChordLength; // We have no intersection if the segment is lying outside of the circle. if (direction.Dot(currentPoint) + EPS > direction.Dot(intersectionB) || direction.Dot(nextPoint) - EPS < direction.Dot(intersectionA)) continue; lines->emplace_back( direction.Dot(currentPoint) > direction.Dot(intersectionA) ? currentPoint : intersectionA); lines->emplace_back( direction.Dot(nextPoint) < direction.Dot(intersectionB) ? nextPoint : intersectionB); } } void DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float mapScale) { // Rotate the texture coordinates (0,0)-(coordMax,coordMax) around their center point (m,m) // Scale square maps to fit in circular minimap area const float s = sin(angle) * mapScale; const float c = cos(angle) * mapScale; const float m = coordMax / 2.f; float quadTex[] = { m*(-c + s + 1.f), m*(-c + -s + 1.f), m*(c + s + 1.f), m*(-c + s + 1.f), m*(c + -s + 1.f), m*(c + s + 1.f), m*(c + -s + 1.f), m*(c + s + 1.f), m*(-c + -s + 1.f), m*(c + -s + 1.f), m*(-c + s + 1.f), m*(-c + -s + 1.f) }; float quadVerts[] = { x, y, 0.0f, x2, y, 0.0f, x2, y2, 0.0f, x2, y2, 0.0f, x, y2, 0.0f, x, y, 0.0f }; shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, quadTex); shader->VertexPointer(3, GL_FLOAT, 0, quadVerts); shader->AssertPointersBound(); if (!g_Renderer.DoSkipSubmit()) glDrawArrays(GL_TRIANGLES, 0, 6); } } // anonymous namespace const CStr CMiniMap::EventNameWorldClick = "WorldClick"; CMiniMap::CMiniMap(CGUI& pGUI) : IGUIObject(pGUI), m_MapSize(0), m_MapScale(1.f), m_EntitiesDrawn(0), m_IndexArray(GL_STATIC_DRAW), m_VertexArray(GL_DYNAMIC_DRAW), m_Mask(this, "mask", false), - m_NextBlinkTime(0.0), m_PingDuration(25.0), m_BlinkState(false) + m_NextBlinkTime(0.0), m_PingDuration(25.0), m_BlinkState(false), + m_FlareTextureCount(this, "flare_texture_count", 0), m_FlareRenderSize(this, "flare_render_size", 0), + m_FlareAnimationSpeed(this, "flare_animation_speed", 0), m_FlareInterleave(this, "flare_interleave", false), + m_FlareLifetimeSeconds(this, "flare_lifetime_seconds", 0) { m_Clicking = false; m_MouseHovering = false; m_AttributePos.type = GL_FLOAT; m_AttributePos.elems = 2; m_VertexArray.AddAttribute(&m_AttributePos); m_AttributeColor.type = GL_UNSIGNED_BYTE; m_AttributeColor.elems = 4; m_VertexArray.AddAttribute(&m_AttributeColor); m_VertexArray.SetNumVertices(MAX_ENTITIES_DRAWN); m_VertexArray.Layout(); m_IndexArray.SetNumVertices(MAX_ENTITIES_DRAWN); m_IndexArray.Layout(); VertexArrayIterator index = m_IndexArray.GetIterator(); for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i) *index++ = i; m_IndexArray.Upload(); m_IndexArray.FreeBackingStore(); VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i) { (*attrColor)[0] = 0; (*attrColor)[1] = 0; (*attrColor)[2] = 0; (*attrColor)[3] = 0; ++attrColor; (*attrPos)[0] = -10000.0f; (*attrPos)[1] = -10000.0f; ++attrPos; } m_VertexArray.Upload(); double blinkDuration = 1.0; // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("gui.session.minimap.pingduration", m_PingDuration); CFG_GET_VAL("gui.session.minimap.blinkduration", blinkDuration); } m_HalfBlinkDuration = blinkDuration/2; } CMiniMap::~CMiniMap() = default; void CMiniMap::HandleMessage(SGUIMessage& Message) { IGUIObject::HandleMessage(Message); switch (Message.type) { + case GUIM_LOAD: + RecreateFlareTextures(); + break; + case GUIM_SETTINGS_UPDATED: + if (Message.value == "flare_texture_count") + RecreateFlareTextures(); + break; case GUIM_MOUSE_PRESS_LEFT: if (m_MouseHovering) { if (!CMiniMap::FireWorldClickEvent(SDL_BUTTON_LEFT, 1)) { SetCameraPositionFromMousePosition(); m_Clicking = true; } } break; case GUIM_MOUSE_RELEASE_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); m_Clicking = false; break; case GUIM_MOUSE_DBLCLICK_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); m_Clicking = false; break; case GUIM_MOUSE_ENTER: m_MouseHovering = true; break; case GUIM_MOUSE_LEAVE: m_Clicking = false; m_MouseHovering = false; break; case GUIM_MOUSE_RELEASE_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 1); break; case GUIM_MOUSE_DBLCLICK_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 2); break; case GUIM_MOUSE_MOTION: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); break; case GUIM_MOUSE_WHEEL_DOWN: case GUIM_MOUSE_WHEEL_UP: Message.Skip(); break; default: break; } } +void CMiniMap::RecreateFlareTextures() +{ + // Catch invalid values. + if (m_FlareTextureCount > 99) + { + LOGERROR("Invalid value for flare texture count. Valid range is 0-99."); + return; + } + const CStr numberingFormat = "%02u"; + m_FlareTextures.clear(); + m_FlareTextures.reserve(m_FlareTextureCount); + for (u32 i = 0; i < m_FlareTextureCount; ++i) + { + CTextureProperties textureProps(L"art/textures/animated/minimap-flare/frame" + CStr(fmt::sprintf(numberingFormat, i)).FromUTF8() + L".png"); + m_FlareTextures.emplace_back(g_Renderer.GetTextureManager().CreateTexture(textureProps)); + } +} + bool CMiniMap::IsMouseOver() const { // Get the mouse position. const CVector2D& mousePos = m_pGUI.GetMousePos(); // Get the position of the center of the minimap. CVector2D minimapCenter = CVector2D(m_CachedActualSize.left + m_CachedActualSize.GetWidth() / 2.0, m_CachedActualSize.bottom - m_CachedActualSize.GetHeight() / 2.0); // Take the magnitude of the difference of the mouse position and minimap center. double distFromCenter = sqrt(pow((mousePos.X - minimapCenter.X), 2) + pow((mousePos.Y - minimapCenter.Y), 2)); // If the distance is less then the radius of the minimap (half the width) the mouse is over the minimap. if (distFromCenter < m_CachedActualSize.GetWidth() / 2.0) return true; else return false; } void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) const { // Determine X and Z according to proportion of mouse position and minimap. const CVector2D& mousePos = m_pGUI.GetMousePos(); float px = (mousePos.X - m_CachedActualSize.left) / m_CachedActualSize.GetWidth(); float py = (m_CachedActualSize.bottom - mousePos.Y) / m_CachedActualSize.GetHeight(); float angle = GetAngle(); // Scale world coordinates for shrunken square map x = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(px-0.5) - sin(angle)*(py-0.5)) + 0.5); z = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(py-0.5) + sin(angle)*(px-0.5)) + 0.5); } void CMiniMap::SetCameraPositionFromMousePosition() { CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CVector3D target; GetMouseWorldCoordinates(target.X, target.Z); target.Y = terrain->GetExactGroundLevel(target.X, target.Z); g_Game->GetView()->MoveCameraTarget(target); } float CMiniMap::GetAngle() const { CVector3D cameraIn = m_Camera->GetOrientation().GetIn(); return -atan2(cameraIn.X, cameraIn.Z); } bool CMiniMap::FireWorldClickEvent(int button, int UNUSED(clicks)) { ScriptRequest rq(g_GUI->GetActiveGUI()->GetScriptInterface()); float x, z; GetMouseWorldCoordinates(x, z); JS::RootedValue coords(rq.cx); Script::CreateObject(rq, &coords, "x", x, "z", z); JS::RootedValue buttonJs(rq.cx); Script::ToJSVal(rq, &buttonJs, button); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(coords)); ignore_result(paramData.append(buttonJs)); return ScriptEventWithReturn(EventNameWorldClick, paramData); } // This sets up and draws the rectangle on the minimap // which represents the view of the camera in the world. void CMiniMap::DrawViewRect(const CMatrix3D& transform) const { // Compute the camera frustum intersected with a fixed-height plane. // Use the water height as a fixed base height, which should be the lowest we can go float h = g_Renderer.GetWaterManager()->m_WaterHeight; const float width = m_CachedActualSize.GetWidth(); const float height = m_CachedActualSize.GetHeight(); const float invTileMapSize = 1.0f / float(TERRAIN_TILE_SIZE * m_MapSize); const std::array hitPoints = { m_Camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), h), m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), h), m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, h), m_Camera->GetWorldCoordinates(0, 0, h) }; std::vector lines; // We need to prevent drawing view bounds out of the map. const float halfMapSize = static_cast((m_MapSize - 1) * TERRAIN_TILE_SIZE) * 0.5f; CropPointsByCircle(hitPoints, CVector3D(halfMapSize, 0.0f, halfMapSize), halfMapSize * m_MapScale, &lines); if (lines.empty()) return; std::vector vertices; vertices.reserve(lines.size() * 2); for (const CVector3D& point : lines) { // Convert to minimap space. vertices.emplace_back(width * point.X * invTileMapSize); vertices.emplace_back(-(height * point.Z * invTileMapSize)); } glLineWidth(2.0f); CShaderDefines lineDefines; lineDefines.Add(str_MINIMAP_LINE, str_1); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), lineDefines); tech->BeginPass(); CShaderProgramPtr shader = tech->GetShader(); shader->Uniform(str_transform, transform); shader->Uniform(str_color, 1.0f, 0.3f, 0.3f, 1.0f); shader->VertexPointer(2, GL_FLOAT, 0, vertices.data()); shader->AssertPointersBound(); if (!g_Renderer.DoSkipSubmit()) glDrawArrays(GL_LINES, 0, vertices.size() / 2); tech->EndPass(); glLineWidth(1.0f); } +void CMiniMap::DrawFlare(CCanvas2D& canvas, const MapFlare& flare, double currentTime) const +{ + if (!m_FlareTextures.size()) + return; + + // Coordinates with 0,0 in the middle of the minimap and +-0.5 as max. + const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize); + const float relativeX = (flare.pos.X * invTileMapSize - 0.5) / m_MapScale; + const float relativeY = (flare.pos.Y * invTileMapSize - 0.5) / m_MapScale; + + // Rotate coordinates. + const float angle = GetAngle(); + const float rotatedX = cos(angle) * relativeX + sin(angle) * relativeY; + const float rotatedY = -sin(angle) * relativeX + cos(angle) * relativeY; + // Calculate coordinates in gui space. + const float cx = m_CachedActualSize.left + (0.5f + rotatedX) * m_CachedActualSize.GetWidth(); + const float cy = m_CachedActualSize.bottom - (0.5f + rotatedY) * m_CachedActualSize.GetHeight(); + + const CRect destination(cx-m_FlareRenderSize, cy-m_FlareRenderSize, cx+m_FlareRenderSize, cy+m_FlareRenderSize); + + const u32 flooredStep = floor((currentTime - flare.time) * m_FlareAnimationSpeed); + + CTexturePtr texture = m_FlareTextures[flooredStep % m_FlareTextures.size()]; + // TODO: Only draw inside the minimap circle. + canvas.DrawTexture(texture, destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()), flare.color, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f); + + // Draw a second circle if the first has reached half of the animation + if (m_FlareInterleave && flooredStep >= m_FlareTextures.size() / 2) + { + texture = m_FlareTextures[(flooredStep - m_FlareTextures.size() / 2) % m_FlareTextures.size()]; + // TODO: Only draw inside the minimap circle. + canvas.DrawTexture(texture, destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()), flare.color, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f); + } +} + struct MinimapUnitVertex { // This struct is copyable for convenience and because to move is to copy for primitives. u8 r, g, b, a; float x, y; }; // Adds a vertex to the passed VertexArray static void inline addVertex(const MinimapUnitVertex& v, VertexArrayIterator& attrColor, VertexArrayIterator& attrPos) { (*attrColor)[0] = v.r; (*attrColor)[1] = v.g; (*attrColor)[2] = v.b; (*attrColor)[3] = v.a; ++attrColor; (*attrPos)[0] = v.x; (*attrPos)[1] = v.y; ++attrPos; } // TODO: render the minimap in a framebuffer and just draw the frambuffer texture // most of the time, updating the framebuffer twice a frame. // Here it updates as ping-pong either texture or vertex array each sec to lower gpu stalling // (those operations cause a gpu sync, which slows down the way gpu works) void CMiniMap::Draw(CCanvas2D& canvas) { PROFILE3("render minimap"); // The terrain isn't actually initialized until the map is loaded, which // happens when the game is started, so abort until then. if (!g_Game || !g_Game->IsGameStarted()) return; canvas.Flush(); CSimulation2* sim = g_Game->GetSimulation2(); CmpPtr cmpRangeManager(*sim, SYSTEM_ENTITY); ENSURE(cmpRangeManager); CLOSTexture& losTexture = g_Game->GetView()->GetLOSTexture(); CMiniMapTexture& miniMapTexture = g_Game->GetView()->GetMiniMapTexture(); // Set our globals in case they hadn't been set before m_Camera = g_Game->GetView()->GetCamera(); const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); ssize_t width = (u32)(m_CachedActualSize.right - m_CachedActualSize.left); ssize_t height = (u32)(m_CachedActualSize.bottom - m_CachedActualSize.top); m_MapSize = terrain->GetVerticesPerSide(); GLsizei textureSize = miniMapTexture.GetTerrainTextureSize(); m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f); // only update 2x / second // (note: since units only move a few pixels per second on the minimap, // we can get away with infrequent updates; this is slow) // TODO: Update all but camera at same speed as simulation static double last_time; const double cur_time = timer_Time(); const bool doUpdate = cur_time - last_time > 0.5; if (doUpdate) last_time = cur_time; const float x = m_CachedActualSize.left, y = m_CachedActualSize.bottom; const float x2 = m_CachedActualSize.right, y2 = m_CachedActualSize.top; const float texCoordMax = (float)(m_MapSize - 1) / (float)textureSize; const float angle = GetAngle(); const float unitScale = (cmpRangeManager->GetLosCircular() ? 1.f : m_MapScale/2.f); CShaderProgramPtr shader; CShaderTechniquePtr tech; CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); if (m_Mask) baseDefines.Add(str_MINIMAP_MASK, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), baseDefines); tech->BeginPass(); shader = tech->GetShader(); // Draw the main textured quad if (miniMapTexture.GetTerrainTexture()) shader->BindTexture(str_baseTex, miniMapTexture.GetTerrainTexture()); if (m_Mask) { shader->BindTexture(str_maskTex, losTexture.GetTexture()); CMatrix3D maskTextureTransform = *losTexture.GetMinimapTextureMatrix(); // We need to have texture coordinates in the same coordinate space. const float scale = 1.0f / texCoordMax; maskTextureTransform.Scale(scale, scale, 1.0f); shader->Uniform(str_maskTextureTransform, maskTextureTransform); } const CMatrix3D baseTransform = GetDefaultGuiMatrix(); CMatrix3D baseTextureTransform; baseTextureTransform.SetIdentity(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, baseTextureTransform); if (m_Mask) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } if (miniMapTexture.GetTerrainTexture()) DrawTexture(shader, texCoordMax, angle, x, y, x2, y2, m_MapScale); if (!m_Mask) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } // Draw territory boundaries CTerritoryTexture& territoryTexture = g_Game->GetView()->GetTerritoryTexture(); shader->BindTexture(str_baseTex, territoryTexture.GetTexture()); if (m_Mask) { shader->BindTexture(str_maskTex, losTexture.GetTexture()); shader->Uniform(str_maskTextureTransform, *losTexture.GetMinimapTextureMatrix()); } const CMatrix3D* territoryTransform = territoryTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *territoryTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, m_MapScale); tech->EndPass(); // Draw the LOS quad in black, using alpha values from the LOS texture if (!m_Mask) { CShaderDefines losDefines; losDefines.Add(str_MINIMAP_LOS, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), losDefines); tech->BeginPass(); shader = tech->GetShader(); shader->BindTexture(str_baseTex, losTexture.GetTexture()); const CMatrix3D* losTransform = losTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *losTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, m_MapScale); tech->EndPass(); } glDisable(GL_BLEND); - PROFILE_START("minimap units"); + PROFILE_START("minimap units and flares"); CShaderDefines pointDefines; pointDefines.Add(str_MINIMAP_POINT, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), pointDefines); tech->BeginPass(); shader = tech->GetShader(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_pointSize, 3.f); CMatrix3D unitMatrix; unitMatrix.SetIdentity(); // Center the minimap on the origin of the axis of rotation. unitMatrix.Translate(-(x2 - x) / 2.f, -(y2 - y) / 2.f, 0.f); // Rotate the map. unitMatrix.RotateZ(angle); // Scale square maps to fit. unitMatrix.Scale(unitScale, unitScale, 1.f); // Move the minimap back to it's starting position. unitMatrix.Translate((x2 - x) / 2.f, (y2 - y) / 2.f, 0.f); // Move the minimap to it's final location. unitMatrix.Translate(x, y, 0.0f); // Apply the gui matrix. unitMatrix *= GetDefaultGuiMatrix(); // Load the transform into the shader. shader->Uniform(str_transform, unitMatrix); const float sx = (float)width / ((m_MapSize - 1) * TERRAIN_TILE_SIZE); const float sy = (float)height / ((m_MapSize - 1) * TERRAIN_TILE_SIZE); CSimulation2::InterfaceList ents = sim->GetEntitiesWithInterface(IID_Minimap); if (doUpdate) { VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); m_EntitiesDrawn = 0; MinimapUnitVertex v; std::vector pingingVertices; pingingVertices.reserve(MAX_ENTITIES_DRAWN / 2); if (cur_time > m_NextBlinkTime) { m_BlinkState = !m_BlinkState; m_NextBlinkTime = cur_time + m_HalfBlinkDuration; } entity_pos_t posX, posZ; for (CSimulation2::InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) { ICmpMinimap* cmpMinimap = static_cast(it->second); if (cmpMinimap->GetRenderData(v.r, v.g, v.b, posX, posZ)) { LosVisibility vis = cmpRangeManager->GetLosVisibility(it->first, g_Game->GetSimulation2()->GetSimContext().GetCurrentDisplayedPlayer()); if (vis != LosVisibility::HIDDEN) { v.a = 255; v.x = posX.ToFloat() * sx; v.y = -posZ.ToFloat() * sy; // Check minimap pinging to indicate something if (m_BlinkState && cmpMinimap->CheckPing(cur_time, m_PingDuration)) { v.r = 255; // ping color is white v.g = 255; v.b = 255; pingingVertices.push_back(v); } else { addVertex(v, attrColor, attrPos); ++m_EntitiesDrawn; } } } } // Add the pinged vertices at the end, so they are drawn on top for (const MinimapUnitVertex& vertex : pingingVertices) { addVertex(vertex, attrColor, attrPos); ++m_EntitiesDrawn; } ENSURE(m_EntitiesDrawn < MAX_ENTITIES_DRAWN); m_VertexArray.Upload(); } m_VertexArray.PrepareForRendering(); if (m_EntitiesDrawn > 0) { #if !CONFIG2_GLES glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif u8* indexBase = m_IndexArray.Bind(); u8* base = m_VertexArray.Bind(); const GLsizei stride = (GLsizei)m_VertexArray.GetStride(); shader->VertexPointer(2, GL_FLOAT, stride, base + m_AttributePos.offset); shader->ColorPointer(4, GL_UNSIGNED_BYTE, stride, base + m_AttributeColor.offset); shader->AssertPointersBound(); if (!g_Renderer.DoSkipSubmit()) glDrawElements(GL_POINTS, (GLsizei)(m_EntitiesDrawn), GL_UNSIGNED_SHORT, indexBase); g_Renderer.GetStats().m_DrawCalls++; CVertexBuffer::Unbind(); #if !CONFIG2_GLES glDisable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif } tech->EndPass(); DrawViewRect(unitMatrix); - PROFILE_END("minimap units"); + while (!m_MapFlares.empty() && m_FlareLifetimeSeconds + m_MapFlares.front().time < cur_time) + m_MapFlares.pop_front(); + + for (const MapFlare& flare : m_MapFlares) + DrawFlare(canvas, flare, cur_time); + + PROFILE_END("minimap units and flares"); +} + +bool CMiniMap::Flare(const CVector2D& pos, const CStr& colorStr) +{ + CColor color; + if (!color.ParseString(colorStr)) + { + LOGERROR("CMiniMap::Flare: Couldn't parse color string"); + return false; + } + m_MapFlares.push_back({ pos, color, timer_Time() }); + return true; } Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 25690) +++ ps/trunk/binaries/data/config/default.cfg (revision 25691) @@ -1,562 +1,564 @@ ; Global Configuration Settings ; ; ************************************************************** ; * DO NOT EDIT THIS FILE if you want personal customisations: * ; * create a text file called "local.cfg" instead, and copy * ; * the lines from this file that you want to change. * ; * * ; * If a setting is part of a section (for instance [hotkey]) * ; * you need to append the section name at the beginning of * ; * your custom line (for instance you need to write * ; * "hotkey.pause = Space" if you want to change the pausing * ; * hotkey to the spacebar). * ; * * ; * On Linux, create: * ; * $XDG_CONFIG_HOME/0ad/config/local.cfg * ; * (Note: $XDG_CONFIG_HOME defaults to ~/.config) * ; * * ; * On OS X, create: * ; * ~/Library/Application\ Support/0ad/config/local.cfg * ; * * ; * On Windows, create: * ; * %appdata%\0ad\config\local.cfg * ; * * ; ************************************************************** ; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.) windowed = false ; Show detailed tooltips (Unit stats) showdetailedtooltips = false ; Pause the game on window focus loss (Only applicable to single player mode) pauseonfocusloss = true ; Persist settings after leaving the game setup screen persistmatchsettings = true ; Default player name to use in multiplayer ; playername = "anonymous" ; Default server name or IP to use in multiplayer multiplayerserver = "127.0.0.1" ; Force a particular resolution. (If these are 0, the default is ; to keep the current desktop resolution in fullscreen mode or to ; use 1024x768 in windowed mode.) xres = 0 yres = 0 ; Force a non-standard bit depth (if 0 then use the current desktop bit depth) bpp = 0 ; Preferred display (for multidisplay setups, only works with SDL 2.0) display = 0 ; Allows to force GL version for SDL forceglversion = false forceglprofile = "compatibility" ; Possible values: compatibility, core, es forceglmajorversion = 3 forceglminorversion = 3 ; Big screenshot tiles screenshot.tiles = 4 screenshot.tilewidth = 480 screenshot.tileheight = 270 ; Emulate right-click with Ctrl+Click on Mac mice macmouse = false ; System settings: ; if false, actors won't be rendered but anything entity will be. renderactors = true watereffects=true ; When disabled, force usage of the fixed pipeline water. This is faster, but really, really ugly. waterfancyeffects = false waterrealdepth = true waterrefraction = true waterreflection = true shadows = true shadowquality = 0 ; Shadow map resolution. (-2 - Very Low, -1 - Low, 0 - Medium, 1 - High, 2 - Very High) ; High values can crash the game when using a graphics card with low memory! shadowpcf = true shadowsfixed = false ; When enabled shadows are rendered only on the shadowsfixeddistance = 300.0 ; fixed distance and without swimming effect. texturequality = 5 ; Texture resolution and quality (0 - Lowest, 1 Very Low, 2 - Low, 3 - Medium, 4 - High, 5 - Very High, 6 - Ultra) vsync = false particles = true fog = true silhouettes = true showsky = true ; Uses a synchonized call to a GL driver to get an error state. Useful ; for a debugging of a system without GL_KHR_debug. gl.checkerrorafterswap = false ; Disable hardware cursors nohwcursor = false ; Specify the render path. This can be one of: ; default Automatically select one of the below, depending on system capabilities ; fixed Only use OpenGL fixed function pipeline ; shader Use vertex/fragment shaders for transform and lighting where possible ; Using 'fixed' instead of 'default' may work around some graphics-related problems, ; but will reduce performance and features when a modern graphics card is available. renderpath = default ;;;;; EXPERIMENTAL ;;;;; ; Prefer GLSL shaders over ARB shaders. Allows fancier graphical effects. preferglsl = false ; Experimental probably-non-working GPU skinning support; requires preferglsl; use at own risk gpuskinning = false ; Use smooth LOS interpolation smoothlos = false ; Use screen-space postprocessing filters (HDR, bloom, DOF, etc). Incompatible with fixed renderpath. postproc = false ; Use anti-aliasing techniques. antialiasing = "disabled" ; Use sharpening techniques. sharpening = "disabled" sharpness = 0.3 ; Quality used for actors. max_actor_quality=200 ; Whether or not actor variants are selected randomly, possible values are "full", "limited", "none". variant_diversity = "full" ; Quality level of shader effects (set to 10 to display all effects) materialmgr.quality = 2.0 ; Maximum distance to display parallax effect. Set to 0 to disable parallax. materialmgr.PARALLAX_DIST.max = 150 ; Maximum distance to display high quality parallax effect. materialmgr.PARALLAX_HQ_DIST.max = 75 ; Maximum distance to display very high quality parallax effect. Set to 30 to enable. materialmgr.PARALLAX_VHQ_DIST.max = 0 ;;;;;;;;;;;;;;;;;;;;;;;; ; Replace alpha-blending with alpha-testing, for performance experiments forcealphatest = false ; Color of the sky (in "r g b" format) skycolor = "0 0 0" [adaptivefps] session = 60 ; Throttle FPS in running games (prevents 100% CPU workload). menu = 60 ; Throttle FPS in menus only. [profiler2] server = "127.0.0.1" server.port = "8000" ; Use a free port on your machine. server.threads = "6" ; Enough for the browser's parallel connection limit [hotkey] ; Each one of the specified keys will trigger the action on the left ; for multiple-key combinations, separate keys with '+'. ; See keys.txt for the list of key names. ; > SYSTEM SETTINGS exit = "Ctrl+Break", "Super+Q", "Alt+F4" ; Exit to desktop cancel = Escape ; Close or cancel the current dialog box/popup confirm = Return ; Confirm the current command pause = Pause, "Shift+Space" ; Pause/unpause game screenshot = F2 ; Take PNG screenshot bigscreenshot = "Shift+F2" ; Take large BMP screenshot togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode screenshot.watermark = "Alt+K" ; Toggle product/company watermark for official screenshots wireframe = "Alt+Shift+W" ; Toggle wireframe mode silhouettes = "Alt+Shift+S" ; Toggle unit silhouettes ; > DIALOG HOTKEYS summary = "Ctrl+Tab" ; Toggle in-game summary lobby = "Alt+L" ; Show the multiplayer lobby in a dialog window. structree = "Alt+Shift+T" ; Show structure tree civinfo = "Alt+Shift+H" ; Show civilization info ; > CLIPBOARD CONTROLS copy = "Ctrl+C" ; Copy to clipboard paste = "Ctrl+V" ; Paste from clipboard cut = "Ctrl+X" ; Cut selected text and copy to the clipboard ; > CONSOLE SETTINGS console.toggle = BackQuote, F9 ; Open/close console ; > OVERLAY KEYS fps.toggle = "Alt+F" ; Toggle frame counter realtime.toggle = "Alt+T" ; Toggle current display of computer time timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter ceasefirecounter.toggle = "" ; Toggle ceasefire counter ; > HOTKEYS ONLY chat = Return ; Toggle chat window teamchat = "T" ; Toggle chat window in team chat mode privatechat = "L" ; Toggle chat window and select the previous private chat partner ; > QUICKSAVE quicksave = "Shift+F5" quickload = "Shift+F8" [hotkey.camera] reset = "R" ; Reset camera rotation to default. follow = "F" ; Follow the first unit in the selection rallypointfocus = "" ; Focus the camera on the rally point of the selected building zoom.in = Plus, NumPlus ; Zoom camera in (continuous control) zoom.out = Minus, NumMinus ; Zoom camera out (continuous control) zoom.wheel.in = WheelUp ; Zoom camera in (stepped control) zoom.wheel.out = WheelDown ; Zoom camera out (stepped control) rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control) rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control) pan = MouseMiddle ; Enable scrolling by moving mouse left = A, LeftArrow ; Scroll or rotate left right = D, RightArrow ; Scroll or rotate right up = W, UpArrow ; Scroll or rotate up/forwards down = S, DownArrow ; Scroll or rotate down/backwards scroll.speed.increase = "Ctrl+Shift+S" ; Increase scroll speed scroll.speed.decrease = "Ctrl+Alt+S" ; Decrease scroll speed rotate.speed.increase = "Ctrl+Shift+R" ; Increase rotation speed rotate.speed.decrease = "Ctrl+Alt+R" ; Decrease rotation speed zoom.speed.increase = "Ctrl+Shift+Z" ; Increase zoom speed zoom.speed.decrease = "Ctrl+Alt+Z" ; Decrease zoom speed [hotkey.camera.jump] 1 = F5 ; Jump to position N 2 = F6 3 = F7 4 = F8 ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.camera.jump.set] 1 = "Ctrl+F5" ; Set jump position N 2 = "Ctrl+F6" 3 = "Ctrl+F7" 4 = "Ctrl+F8" ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.profile] toggle = "F11" ; Enable/disable real-time profiler save = "Shift+F11" ; Save current profiler data to logs/profile.txt [hotkey.profile2] toggle = "Ctrl+F11" ; Enable/disable HTTP/GPU modes for new profiler [hotkey.selection] cancel = Esc ; Un-select all units and cancel building placement add = Shift ; Add units to selection militaryonly = Alt ; Add only military units to the selection nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection idleonly = "I" ; Select only idle units woundedonly = "O" ; Select only wounded units remove = Ctrl ; Remove units from selection idlebuilder = Semicolon ; Select next idle builder idleworker = Period, NumDecimal ; Select next idle worker idlewarrior = Slash, NumDivide ; Select next idle warrior idleunit = BackSlash ; Select next idle unit offscreen = Alt ; Include offscreen units in selection [hotkey.selection.group.add] 0 = "Shift+0", "Shift+Num0" 1 = "Shift+1", "Shift+Num1" 2 = "Shift+2", "Shift+Num2" 3 = "Shift+3", "Shift+Num3" 4 = "Shift+4", "Shift+Num4" 5 = "Shift+5", "Shift+Num5" 6 = "Shift+6", "Shift+Num6" 7 = "Shift+7", "Shift+Num7" 8 = "Shift+8", "Shift+Num8" 9 = "Shift+9", "Shift+Num9" [hotkey.selection.group.save] 0 = "Ctrl+0", "Ctrl+Num0" 1 = "Ctrl+1", "Ctrl+Num1" 2 = "Ctrl+2", "Ctrl+Num2" 3 = "Ctrl+3", "Ctrl+Num3" 4 = "Ctrl+4", "Ctrl+Num4" 5 = "Ctrl+5", "Ctrl+Num5" 6 = "Ctrl+6", "Ctrl+Num6" 7 = "Ctrl+7", "Ctrl+Num7" 8 = "Ctrl+8", "Ctrl+Num8" 9 = "Ctrl+9", "Ctrl+Num9" [hotkey.selection.group.select] 0 = 0, Num0 1 = 1, Num1 2 = 2, Num2 3 = 3, Num3 4 = 4, Num4 5 = 5, Num5 6 = 6, Num6 7 = 7, Num7 8 = 8, Num8 9 = 9, Num9 [hotkey.gamesetup] mapbrowser.open = "M" [hotkey.session] kill = Delete, Backspace ; Destroy selected units stop = "H" ; Stop the current action backtowork = "Y" ; The unit will go back to work unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected unloadturrets = "U" ; Unload turreted units. leaveturret = "U" ; Leave turret point. move = "" ; Modifier to move to a point instead of another action (e.g. gather) attack = Ctrl ; Modifier to attack instead of another action (e.g. capture) attackmove = Ctrl ; Modifier to attackmove when clicking on a point attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point garrison = Ctrl ; Modifier to garrison when clicking on building occupyturret = Ctrl ; Modifier to occupy a turret when clicking on a turret holder. autorallypoint = Ctrl ; Modifier to set the rally point on the building itself guard = "G" ; Modifier to escort/guard when clicking on unit/building patrol = "P" ; Modifier to patrol a unit repair = "J" ; Modifier to repair when clicking on building/mechanical unit queue = Shift ; Modifier to queue unit orders instead of replacing pushorderfront = "" ; Modifier to push unit orders to the front instead of replacing. orderone = Alt ; Modifier to order only one entity in selection. batchtrain = Shift ; Modifier to train units in batches massbarter = Shift ; Modifier to barter bunch of resources masstribute = Shift ; Modifier to tribute bunch of resources noconfirmation = Shift ; Do not ask confirmation when deleting a building/unit fulltradeswap = Shift ; Modifier to put the desired trade resource to 100% unloadtype = Shift ; Modifier to unload all units of type deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting rotate.cw = RightBracket ; Rotate building placement preview clockwise rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise snaptoedges = Ctrl ; Modifier to align new structures with nearby existing structure toggledefaultformation = "" ; Switch between null default formation and the last default formation used (defaults to "box") +flare = K ; Modifier to send a flare to your allies +flareactivate = "" ; Modifier to activate the mode to send a flare to your allies ; Overlays showstatusbars = Tab ; Toggle display of status bars devcommands.toggle = "Alt+D" ; Toggle developer commands panel highlightguarding = PageDown ; Toggle highlight of guarding units highlightguarded = PageUp ; Toggle highlight of guarded units diplomacycolors = "Alt+X" ; Toggle diplomacy colors toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units [hotkey.session.gui] toggle = "Alt+G" ; Toggle visibility of session GUI menu.toggle = "F10" ; Toggle in-game menu diplomacy.toggle = "Ctrl+H" ; Toggle in-game diplomacy page barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page objectives.toggle = "Ctrl+O" ; Toggle in-game objectives page tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel [hotkey.session.savedgames] delete = Delete, Backspace ; Delete the selected saved game asking confirmation noconfirmation = Shift ; Do not ask confirmation when deleting a game [hotkey.session.queueunit] ; > UNIT TRAINING 1 = "Z" ; add first unit type to queue 2 = "X" ; add second unit type to queue 3 = "C" ; add third unit type to queue 4 = "V" ; add fourth unit type to queue 5 = "B" ; add fivth unit type to queue 6 = "N" ; add sixth unit type to queue 7 = "M" ; add seventh unit type to queue 8 = Comma ; add eighth unit type to queue [hotkey.session.timewarp] fastforward = Space ; If timewarp mode enabled, speed up the game rewind = "Shift+Backspace" ; If timewarp mode enabled, go back to earlier point in the game [hotkey.tab] next = "Tab", "Alt+S" ; Show the next tab prev = "Shift+Tab", "Alt+W" ; Show the previous tab [hotkey.text] ; > GUI TEXTBOX HOTKEYS delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor delete.right = "Ctrl+Del" ; Delete word to the right of cursor move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor [gui] cursorblinkrate = 0.5 ; Cursor blink rate in seconds (0.0 to disable blinking) scale = 1.0 ; GUI scaling factor, for improved compatibility with 4K displays [gui.gamesetup] enabletips = true ; Enable/Disable tips during gamesetup (for newcomers) assignplayers = everyone ; Whether to assign joining clients to free playerslots. Possible values: everyone, buddies, disabled. aidifficulty = 3 ; Difficulty level, from 0 (easiest) to 5 (hardest) aibehavior = "random" ; Default behavior of the AI (random, balanced, aggressive or defensive) settingsslide = true ; Enable/Disable settings panel slide [gui.loadingscreen] progressdescription = false ; Whether to display the progress percent or a textual description [gui.session] camerajump.threshold = 40 ; How close do we have to be to the actual location in order to jump back to the previous one? timeelapsedcounter = false ; Show the game duration in the top right corner ceasefirecounter = false ; Show the remaining ceasefire time in the top right corner batchtrainingsize = 5 ; Number of units to be trained per batch by default (when pressing the hotkey) scrollbatchratio = 1 ; Number of times you have to scroll to increase/decrease the batchsize by 1 woundedunithotkeythreshold = 33 ; The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number attackrange = true ; Display attack range overlays of selected defensive structures aurasrange = true ; Display aura range overlays of selected units and structures healrange = true ; Display heal range overlays of selected units rankabovestatusbar = true ; Show rank icons above status bars experiencestatusbar = true ; Show an experience status bar above each selected unit respoptooltipsort = 0 ; Sorting players in the resources and population tooltip by value (0 - no sort, -1 - ascending, 1 - descending) snaptoedges = "disabled" ; Possible values: disabled, enabled. snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges disjointcontrolgroups = "true" ; Whether control groups are disjoint sets or entities can be in multiple control groups at the same time. defaultformation = "special/formations/box" ; For walking orders, automatically put units into this formation if they don't have one already. formationwalkonly = "true" ; Formations are disabled when giving gather/attack/... orders. howtoshownames = 0 ; Whether the specific names are show as default, as opposed to the generic names. And whether the secondary names are shown. (0 - show both; specific names primary, 1 - show both; generic names primary, 2 - show only specific names, 3 - show only generic names) [gui.session.minimap] blinkduration = 1.7 ; The blink duration while pinging pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification [gui.session.notifications] attack = true ; Show a chat notification if you are attacked by another player tribute = true ; Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode barter = true ; Show a chat notification to observers when a player bartered resources phase = completed ; Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode. Possible values: none, completed, all. [gui.splashscreen] enable = true ; Enable/disable the splashscreen version = 0 ; Splashscreen version (date of last modification). By default, 0 to force splashscreen to appear at first launch [gui.session.diplomacycolors] self = "21 55 149" ; Color of your units when diplomacy colors are enabled ally = "86 180 31" ; Color of allies when diplomacy colors are enabled neutral = "231 200 5" ; Color of neutral players when diplomacy colors are enabled enemy = "150 20 20" ; Color of enemies when diplomacy colors are enabled [joystick] ; EXPERIMENTAL: joystick/gamepad settings enable = false deadzone = 8192 [chat] timestamp = true ; Show at which time chat messages have been sent [chat.session] extended = true ; Whether to display the chat history [lobby] history = 0 ; Number of past messages to display on join room = "arena25" ; Default MUC room to join server = "lobby.wildfiregames.com" ; Address of lobby server tls = true ; Whether to use TLS encryption when connecting to the server. verify_certificate = false ; Whether to reject connecting to the lobby if the TLS certificate is invalid (TODO: wait for Gloox GnuTLS trust implementation to be fixed) terms_url = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/prelobby/common/terms/"; Allows the user to save the text and print the terms terms_of_service = "0" ; Version (hash) of the Terms of Service that the user has accepted terms_of_use = "0" ; Version (hash) of the Terms of Use that the user has accepted privacy_policy = "0" ; Version (hash) of the Privacy Policy that the user has accepted xpartamupp = "wfgbot25" ; Name of the server-side XMPP-account that manage games echelon = "echelon25" ; Name of the server-side XMPP-account that manages ratings buddies = "," ; Comma separated list of playernames that the current user has marked as buddies rememberpassword = true ; Whether to store the encrypted password in the user config [lobby.columns] gamerating = false ; Show the average rating of the participating players in a column of the gamelist [lobby.stun] enabled = true ; The STUN protocol allows hosting games without configuring the firewall and router. ; If STUN is disabled, the game relies on direct connection, UPnP and port forwarding. server = "lobby.wildfiregames.com" ; Address of the STUN server. port = 3478 ; Port of the STUN server. delay = 200 ; Duration in milliseconds that is waited between STUN messages. ; Smaller numbers speed up joins but also become less stable. [mod] enabledmods = "mod public" [modio] public_key = "RWRcbM/EwV7bucTiQVCcRBhCkYkXmJEO7s4ktyufkB+gW/NxHhOZ38xh" ; Public key corresponding to the private key valid mods are signed with disclaimer = "0" ; Version (hash) of the Disclaimer that the user has accepted [modio.v1] baseurl = "https://api.mod.io/v1" api_key = "23df258a71711ea6e4b50893acc1ba55" name_id = "0ad" [network] duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join. lateobservers = everyone ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled. observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached observermaxlag = 10 ; Make clients wait for observers if they lag more than X turns behind. -1 means "never wait for observers". autocatchup = true ; Auto-accelerate the sim rate if lagging behind (as an observer). [overlay] fps = "false" ; Show frames per second in top right corner realtime = "false" ; Show current system time in top right corner netwarnings = "true" ; Show warnings if the network connection is bad [profiler2] autoenable = false ; Enable HTTP server output at startup (default off for security/performance) gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available [rlinterface] address = "127.0.0.1:6000" [sound] mastergain = 0.9 musicgain = 0.2 ambientgain = 0.6 actiongain = 0.7 uigain = 0.7 mindistance = 1 maxdistance = 350 maxstereoangle = 0.62 ; About PI/5 radians [sound.notify] nick = true ; Play a sound when someone mentions your name in the lobby or game gamesetup.join = false ; Play a sound when a new client joins the game setup [tinygettext] debug = false ; Print error messages each time a translation for an English string is not found. [userreport] ; Opt-in online user reporting system url_upload = "https://feedback.wildfiregames.com/report/upload/v1/" ; URL where UserReports are uploaded to url_publication = "https://feedback.wildfiregames.com/" ; URL where UserReports were analyzed and published url_terms = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/userreport/Terms_and_Conditions.txt"; Allows the user to save the text and print the terms terms = "0" ; Version (hash) of the UserReporter Terms that the user has accepted [view] ; Camera control settings scroll.speed = 120.0 scroll.speed.modifier = 1.05 ; Multiplier for changing scroll speed rotate.x.speed = 1.2 rotate.x.min = 28.0 rotate.x.max = 60.0 rotate.x.default = 35.0 rotate.y.speed = 2.0 rotate.y.speed.wheel = 0.45 rotate.y.default = 0.0 rotate.speed.modifier = 1.05 ; Multiplier for changing rotation speed drag.speed = 0.5 zoom.speed = 256.0 zoom.speed.wheel = 32.0 zoom.min = 50.0 zoom.max = 200.0 zoom.default = 120.0 zoom.speed.modifier = 1.05 ; Multiplier for changing zoom speed pos.smoothness = 0.1 zoom.smoothness = 0.4 rotate.x.smoothness = 0.5 rotate.y.smoothness = 0.3 near = 2.0 ; Near plane distance far = 4096.0 ; Far plane distance fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide height.smoothness = 0.5 height.min = 16 Index: ps/trunk/binaries/data/mods/mod/gui/gui.rng =================================================================== --- ps/trunk/binaries/data/mods/mod/gui/gui.rng (revision 25690) +++ ps/trunk/binaries/data/mods/mod/gui/gui.rng (revision 25691) @@ -1,874 +1,899 @@ true false left center right top center bottom repeat mirrored_repeat clamp_to_edge -?\d*\.?\d+%?([\+\-]\d*\.?\d+%?)* 0 255 0 255 0 255 0 255 [A-Za-z]+ 0 + + + + + + + + + + + + + + + + + + + + + + + + + 1 1 Index: ps/trunk/binaries/data/mods/public/art/actors/special/flare_target_marker.xml =================================================================== --- ps/trunk/binaries/data/mods/public/art/actors/special/flare_target_marker.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/actors/special/flare_target_marker.xml (revision 25691) @@ -0,0 +1,18 @@ + + + + + + + + + + skeletal/flare_target_marker.dae + + + + + + + player_trans_spec.xml + Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 25690) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 25691) @@ -1,1660 +1,1711 @@ const SDL_BUTTON_LEFT = 1; const SDL_BUTTON_MIDDLE = 2; const SDL_BUTTON_RIGHT = 3; const SDLK_LEFTBRACKET = 91; const SDLK_RIGHTBRACKET = 93; const SDLK_RSHIFT = 303; const SDLK_LSHIFT = 304; const SDLK_RCTRL = 305; const SDLK_LCTRL = 306; const SDLK_RALT = 307; const SDLK_LALT = 308; // TODO: these constants should be defined somewhere else instead, in // case any other code wants to use them too. const ACTION_NONE = 0; const ACTION_GARRISON = 1; const ACTION_REPAIR = 2; const ACTION_GUARD = 3; const ACTION_PATROL = 4; const ACTION_OCCUPY_TURRET = 5; var preSelectedAction = ACTION_NONE; const INPUT_NORMAL = 0; const INPUT_SELECTING = 1; const INPUT_BANDBOXING = 2; const INPUT_BUILDING_PLACEMENT = 3; const INPUT_BUILDING_CLICK = 4; const INPUT_BUILDING_DRAG = 5; const INPUT_BATCHTRAINING = 6; const INPUT_PRESELECTEDACTION = 7; const INPUT_BUILDING_WALL_CLICK = 8; const INPUT_BUILDING_WALL_PATHING = 9; const INPUT_UNIT_POSITION_START = 10; const INPUT_UNIT_POSITION = 11; +const INPUT_FLARE = 12; var inputState = INPUT_NORMAL; const INVALID_ENTITY = 0; var mouseX = 0; var mouseY = 0; var mouseIsOverObject = false; /** * Containing the ingame position which span the line. */ var g_FreehandSelection_InputLine = []; /** * Minimum squared distance when a mouse move is called a drag. */ const g_FreehandSelection_ResolutionInputLineSquared = 1; /** * Minimum length a dragged line should have to use the freehand selection. */ const g_FreehandSelection_MinLengthOfLine = 8; /** * To start the freehandSelection function you need a minimum number of units. * Minimum must be 2, for better performance you could set it higher. */ const g_FreehandSelection_MinNumberOfUnits = 2; /** * Number of pixels the mouse can move before the action is considered a drag. */ const g_MaxDragDelta = 4; /** * Used for remembering mouse coordinates at start of drag operations. */ var g_DragStart; /** * Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities. * If any mousedown or mouseup of a sequence of clicks lands on a unit, * that unit will be selected, which makes it easier to click on moving units. */ var clickedEntity = INVALID_ENTITY; +/** + * Store the last time the flare functionality was used to prevent overusage. + */ +var g_LastFlareTime; + +/** + * The duration in ms for which we disable flaring after each flare to prevent overusage. + */ +const g_FlareCooldown = 5000; + // Same double-click behaviour for hotkey presses. const doublePressTime = 500; var doublePressTimer = 0; var prevHotkey = 0; function updateCursorAndTooltip() { let cursorSet = false; let tooltipSet = false; let informationTooltip = Engine.GetGUIObjectByName("informationTooltip"); - if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) || g_MiniMapPanel.isMouseOverMiniMap()) + if (inputState == INPUT_FLARE || inputState == INPUT_NORMAL && Engine.HotkeyIsPressed("session.flare") && !g_IsObserver) + { + Engine.SetCursor("action-flare"); + cursorSet = true; + } + else if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) || g_MiniMapPanel.isMouseOverMiniMap()) { let action = determineAction(mouseX, mouseY, g_MiniMapPanel.isMouseOverMiniMap()); if (action) { if (action.cursor) { Engine.SetCursor(action.cursor); cursorSet = true; } if (action.tooltip) { tooltipSet = true; informationTooltip.caption = action.tooltip; informationTooltip.hidden = false; } } } if (!cursorSet) Engine.ResetCursor(); if (!tooltipSet) informationTooltip.hidden = true; let placementTooltip = Engine.GetGUIObjectByName("placementTooltip"); if (placementSupport.tooltipMessage) placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip"; placementTooltip.caption = placementSupport.tooltipMessage || ""; placementTooltip.hidden = !placementSupport.tooltipMessage; } function updateBuildingPlacementPreview() { // The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or // in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to. // See onSimulationUpdate in session.js. if (placementSupport.mode === "building") { if (placementSupport.template && placementSupport.position) { let result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed }); placementSupport.tooltipError = !result.success; placementSupport.tooltipMessage = ""; if (!result.success) { if (result.message && result.parameters) { let message = result.message; if (result.translateMessage) if (result.pluralMessage) message = translatePlural(result.message, result.pluralMessage, result.pluralCount); else message = translate(message); let parameters = result.parameters; if (result.translateParameters) translateObjectKeys(parameters, result.translateParameters); placementSupport.tooltipMessage = sprintf(message, parameters); } return false; } if (placementSupport.attack && placementSupport.attack.Ranged) { let cmd = { "x": placementSupport.position.x, "z": placementSupport.position.z, "range": placementSupport.attack.Ranged.maxRange, "elevationBonus": placementSupport.attack.Ranged.elevationBonus }; let averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); let range = Math.round(cmd.range); placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" + sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange }); } return true; } } else if (placementSupport.mode === "wall" && placementSupport.wallSet && placementSupport.position) { placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities( placementSupport.wallSet.templates.tower, placementSupport.wallSnapEntitiesIncludeOffscreen, true, // require exact template match true // include foundations ); return Engine.GuiInterfaceCall("SetWallPlacementPreview", { "wallSet": placementSupport.wallSet, "start": placementSupport.position, "end": placementSupport.wallEndPosition, "snapEntities": placementSupport.wallSnapEntities // snapping entities (towers) for starting a wall segment }); } return false; } /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ function determineAction(x, y, fromMiniMap) { let selection = g_Selection.toList(); if (!selection.length) { preSelectedAction = ACTION_NONE; return undefined; } let entState = GetEntityState(selection[0]); if (!entState) return undefined; if (!selection.every(ownsEntity) && !(g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll)) return undefined; let target; if (!fromMiniMap) { let ent = Engine.PickEntityAtPoint(x, y); if (ent != INVALID_ENTITY) target = ent; } // Decide between the following ordered actions, // if two actions are possible, the first one is taken // thus the most specific should appear first. if (preSelectedAction != ACTION_NONE) { for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].preSelectedActionCheck) { let r = g_UnitActions[action].preSelectedActionCheck(target, selection); if (r) return r; } return { "type": "none", "cursor": "", "target": target }; } for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].hotkeyActionCheck) { let r = g_UnitActions[action].hotkeyActionCheck(target, selection); if (r) return r; } for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].actionCheck) { let r = g_UnitActions[action].actionCheck(target, selection); if (r) return r; } return { "type": "none", "cursor": "", "target": target }; } function ownsEntity(ent) { let entState = GetEntityState(ent); return entState && entState.player == g_ViewedPlayer; } function isAttackMovePressed() { return Engine.HotkeyIsPressed("session.attackmove") || Engine.HotkeyIsPressed("session.attackmoveUnit"); } function isSnapToEdgesEnabled() { let config = Engine.ConfigDB_GetValue("user", "gui.session.snaptoedges"); let hotkeyPressed = Engine.HotkeyIsPressed("session.snaptoedges"); return hotkeyPressed == (config == "disabled"); } function tryPlaceBuilding(queued, pushFront) { if (placementSupport.mode !== "building") { error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'"); return false; } if (!updateBuildingPlacementPreview()) { Engine.GuiInterfaceCall("PlaySound", { "name": "invalid_building_placement", "entity": g_Selection.getFirstSelected() }); return false; } let selection = Engine.HotkeyIsPressed("session.orderone") && popOneFromSelection({ "type": "construct", "target": placementSupport }) || g_Selection.toList(); Engine.PostNetworkCommand({ "type": "construct", "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed, "entities": selection, "autorepair": true, "autocontinue": true, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); if (!queued || !g_Selection.size()) placementSupport.Reset(); else placementSupport.RandomizeActorSeed(); return true; } function tryPlaceWall(queued) { if (placementSupport.mode !== "wall") { error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'"); return false; } let wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) if (!(wallPlacementInfo === false || typeof wallPlacementInfo === "object")) { error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo)); return false; } if (!wallPlacementInfo) return false; let selection = Engine.HotkeyIsPressed("session.orderone") && popOneFromSelection({ "type": "construct", "target": placementSupport }) || g_Selection.toList(); let cmd = { "type": "construct-wall", "autorepair": true, "autocontinue": true, "queued": queued, "entities": selection, "wallSet": placementSupport.wallSet, "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, "formation": g_AutoFormation.getNull() }; // Make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end // point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed // (this is somewhat non-ideal and hardcode-ish). let hasWallSegment = false; for (let piece of cmd.pieces) { if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( { hasWallSegment = true; break; } } if (hasWallSegment) { Engine.PostNetworkCommand(cmd); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); } return true; } /** * Updates the bandbox object with new positions and visibility. * @returns {array} The coordinates of the vertices of the bandbox. */ function updateBandbox(bandbox, ev, hidden) { let scale = +Engine.ConfigDB_GetValue("user", "gui.scale"); let vMin = Vector2D.min(g_DragStart, ev); let vMax = Vector2D.max(g_DragStart, ev); bandbox.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale); bandbox.hidden = hidden; return [vMin.x, vMin.y, vMax.x, vMax.y]; } // Define some useful unit filters for getPreferredEntities. var unitFilters = { "isUnit": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit"); }, "isDefensive": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Defensive"); }, "isMilitary": entity => { let entState = GetEntityState(entity); return entState && g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isNonMilitary": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && !g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isIdle": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && entState.unitAI && entState.unitAI.isIdle && !hasClass(entState, "Domestic"); }, "isWounded": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && entState.maxHitpoints && 100 * entState.hitpoints <= entState.maxHitpoints * Engine.ConfigDB_GetValue("user", "gui.session.woundedunithotkeythreshold"); }, "isAnything": entity => { return true; } }; // Choose, inside a list of entities, which ones will be selected. // We may use several entity filters, until one returns at least one element. function getPreferredEntities(ents) { let filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything]; if (Engine.HotkeyIsPressed("selection.militaryonly")) filters = [unitFilters.isMilitary]; if (Engine.HotkeyIsPressed("selection.nonmilitaryonly")) filters = [unitFilters.isNonMilitary]; if (Engine.HotkeyIsPressed("selection.idleonly")) filters = [unitFilters.isIdle]; if (Engine.HotkeyIsPressed("selection.woundedonly")) filters = [unitFilters.isWounded]; let preferredEnts = []; for (let i = 0; i < filters.length; ++i) { preferredEnts = ents.filter(filters[i]); if (preferredEnts.length) break; } return preferredEnts; } function handleInputBeforeGui(ev, hoveredObject) { if (GetSimState().cinemaPlaying) return false; // Capture cursor position so we can use it for displaying cursors, // and key states. switch (ev.type) { case "mousebuttonup": case "mousebuttondown": case "mousemotion": mouseX = ev.x; mouseY = ev.y; break; } mouseIsOverObject = (hoveredObject != null); // Close the menu when interacting with the game world. if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) g_Menu.close(); // State-machine processing: // // (This is for states which should override the normal GUI processing - events will // be processed here before being passed on, and propagation will stop if this function // returns true) // // TODO: it'd probably be nice to have a better state-machine system, with guaranteed // entry/exit functions, since this is a bit broken now switch (inputState) { case INPUT_BANDBOXING: let bandbox = Engine.GetGUIObjectByName("bandbox"); switch (ev.type) { case "mousemotion": { let rect = updateBandbox(bandbox, ev, false); let ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer); let preferredEntities = getPreferredEntities(ents); g_Selection.setHighlightList(preferredEntities); return false; } case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { let rect = updateBandbox(bandbox, ev, true); let ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer)); g_Selection.setHighlightList([]); if (Engine.HotkeyIsPressed("selection.add")) g_Selection.addList(ents); else if (Engine.HotkeyIsPressed("selection.remove")) g_Selection.removeList(ents); else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } if (ev.button == SDL_BUTTON_RIGHT) { // Cancel selection. bandbox.hidden = true; g_Selection.setHighlightList([]); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_UNIT_POSITION: switch (ev.type) { case "mousemotion": return positionUnitsFreehandSelectionMouseMove(ev); case "mousebuttonup": return positionUnitsFreehandSelectionMouseUp(ev); } break; case INPUT_BUILDING_CLICK: switch (ev.type) { case "mousemotion": // If the mouse moved far enough from the original click location, // then switch to drag-orientation mode. let maxDragDelta = 16; if (g_DragStart.distanceTo(ev) >= maxDragDelta) { inputState = INPUT_BUILDING_DRAG; return false; } break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If queued, let the player continue placing another of the same building. let queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront"))) { if (queued && g_Selection.size()) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else inputState = INPUT_BUILDING_PLACEMENT; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building. placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_CLICK: // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode. switch (ev.type) { case "mousebuttonup": if (ev.button === SDL_BUTTON_LEFT) { inputState = INPUT_BUILDING_WALL_PATHING; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building. placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_PATHING: // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint. // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the // user to continue building walls. switch (ev.type) { case "mousemotion": placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); // Update the structure placement preview, and by extension, the list of snapping candidate entities for both (!) // the ending point and the starting point to snap to. // // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers // in them. Might be useful to query only for entities within a certain range around the starting point and ending // points. placementSupport.wallSnapEntitiesIncludeOffscreen = true; let result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates if (result && result.cost) { let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost }); placementSupport.tooltipMessage = [ getEntityCostTooltip(result), getNeededResourcesTooltip(neededResources) ].filter(tip => tip).join("\n"); } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { let queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceWall(queued)) { if (queued) { // Continue building, just set a new starting position where we left off. placementSupport.position = placementSupport.wallEndPosition; placementSupport.wallEndPosition = undefined; inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.Reset(); inputState = INPUT_NORMAL; } } else placementSupport.tooltipMessage = translate("Cannot build wall here!"); updateBuildingPlacementPreview(); return true; } if (ev.button == SDL_BUTTON_RIGHT) { placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_DRAG: switch (ev.type) { case "mousemotion": let maxDragDelta = 16; if (g_DragStart.distanceTo(ev) >= maxDragDelta) // Rotate in the direction of the cursor. placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); else // If the cursor is near the center, snap back to the default orientation. placementSupport.SetDefaultAngle(); let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "snapToEdges": isSnapToEdgesEnabled() && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( placementSupport.position.x, placementSupport.position.z) }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } updateBuildingPlacementPreview(); break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If queued, let the player continue placing another of the same structure. let queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront"))) { if (queued && g_Selection.size()) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else inputState = INPUT_BUILDING_PLACEMENT; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building. placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BATCHTRAINING: if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain") { flushTrainingBatch(); inputState = INPUT_NORMAL; } break; } return false; } function handleInputAfterGui(ev) { if (GetSimState().cinemaPlaying) return false; if (ev.hotkey === undefined) ev.hotkey = null; if (ev.hotkey == "session.highlightguarding") { g_ShowGuarding = (ev.type == "hotkeypress"); updateAdditionalHighlight(); } else if (ev.hotkey == "session.highlightguarded") { g_ShowGuarded = (ev.type == "hotkeypress"); updateAdditionalHighlight(); } if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING) clickedEntity = INVALID_ENTITY; // State-machine processing: switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousemotion": let ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": + if (Engine.HotkeyIsPressed("session.flare") && controlsPlayer(g_ViewedPlayer)) + { + triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); + return true; + } if (ev.button == SDL_BUTTON_LEFT) { g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_SELECTING; // If a single click occured, reset the clickedEntity. // Also set it if we're double/triple clicking and missed the unit earlier. if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY) clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); return true; } else if (ev.button == SDL_BUTTON_RIGHT) { if (!controlsPlayer(g_ViewedPlayer)) break; g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_UNIT_POSITION_START; } break; case "hotkeypress": if (ev.hotkey.indexOf("selection.group.") == 0) { let now = Date.now(); if (now - doublePressTimer < doublePressTime && ev.hotkey == prevHotkey) { if (ev.hotkey.indexOf("selection.group.select.") == 0) { let sptr = ev.hotkey.split("."); performGroup("snap", sptr[3]); } } else { let sptr = ev.hotkey.split("."); performGroup(sptr[2], sptr[3]); doublePressTimer = now; prevHotkey = ev.hotkey; } } break; } break; case INPUT_PRESELECTEDACTION: switch (ev.type) { case "mousemotion": let ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) { let action = determineAction(ev.x, ev.y); if (!action) break; if (!Engine.HotkeyIsPressed("session.queue") && !Engine.HotkeyIsPressed("session.orderone")) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; } return doAction(action, ev); } if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } default: // Slight hack: If selection is empty, reset the input state. if (!g_Selection.size()) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } } break; case INPUT_SELECTING: switch (ev.type) { case "mousemotion": if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta) { inputState = INPUT_BANDBOXING; return false; } let ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { if (clickedEntity == INVALID_ENTITY) clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); // Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event. if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity)) { clickedEntity = INVALID_ENTITY; if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) { g_Selection.reset(); resetIdleUnit(); } inputState = INPUT_NORMAL; return true; } if (Engine.GetFollowedEntity() != clickedEntity) Engine.CameraFollow(0); let ents = []; if (ev.clicks == 1) ents = [clickedEntity]; else { let showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); let matchRank = true; let templateToMatch; if (ev.clicks == 2) { templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName; if (templateToMatch) matchRank = false; else // No selection group name defined, so fall back to exact match. templateToMatch = GetEntityState(clickedEntity).template; } else // Triple click // Select units matching exact template name (same rank). templateToMatch = GetEntityState(clickedEntity).template; // TODO: Should we handle "control all units" here as well? ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false); } if (Engine.HotkeyIsPressed("selection.add")) g_Selection.addList(ents); else if (Engine.HotkeyIsPressed("selection.remove")) g_Selection.removeList(ents); else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_UNIT_POSITION_START: switch (ev.type) { case "mousemotion": if (g_DragStart.distanceToSquared(ev) >= Math.square(g_MaxDragDelta)) { inputState = INPUT_UNIT_POSITION; return false; } break; case "mousebuttonup": inputState = INPUT_NORMAL; if (ev.button == SDL_BUTTON_RIGHT) { let action = determineAction(ev.x, ev.y); if (action) return doAction(action, ev); } break; } break; case INPUT_BUILDING_PLACEMENT: switch (ev.type) { case "mousemotion": placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (placementSupport.mode === "wall") { // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is // still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities // itself happens in the call to updateBuildingPlacementPreview below.) placementSupport.wallSnapEntitiesIncludeOffscreen = false; } else { if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost })) { placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } if (isSnapToEdgesEnabled()) { // We need to reset the angle before the snapping to edges, // because we want to get the angle near to the default one. placementSupport.SetDefaultAngle(); } let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "snapToEdges": isSnapToEdgesEnabled() && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( placementSupport.position.x, placementSupport.position.z) }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } } updateBuildingPlacementPreview(); // includes an update of the snap entity candidates return false; // continue processing mouse motion case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { if (placementSupport.mode === "wall") { let validPlacement = updateBuildingPlacementPreview(); if (validPlacement !== false) inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (isSnapToEdgesEnabled()) { let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "snapToEdges": Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( placementSupport.position.x, placementSupport.position.z) }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } } g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_BUILDING_CLICK; } return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building. placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; case "hotkeydown": let rotation_step = Math.PI / 12; // 24 clicks make a full rotation switch (ev.hotkey) { case "session.rotate.cw": placementSupport.angle += rotation_step; updateBuildingPlacementPreview(); break; case "session.rotate.ccw": placementSupport.angle -= rotation_step; updateBuildingPlacementPreview(); break; } break; } break; + case INPUT_FLARE: + if (ev.type == "mousebuttondown") + { + if (ev.button == SDL_BUTTON_LEFT && controlsPlayer(g_ViewedPlayer)) + { + triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); + inputState = INPUT_NORMAL; + return true; + } + else if (ev.button == SDL_BUTTON_RIGHT) + { + inputState = INPUT_NORMAL; + return true; + } + } } return false; } function doAction(action, ev) { if (!controlsPlayer(g_ViewedPlayer)) return false; return handleUnitAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y), action); } function popOneFromSelection(action) { // Pick the first unit that can do this order. let unit = action.firstAbleEntity || g_Selection.find(entity => ["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method => g_UnitActions[action.type][method] && g_UnitActions[action.type][method](action.target || undefined, [entity]) )); if (unit) { g_Selection.removeList([unit]); return [unit]; } return null; } function positionUnitsFreehandSelectionMouseMove(ev) { // Converting the input line into a List of points. // For better performance the points must have a minimum distance to each other. let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); if (!g_FreehandSelection_InputLine.length || target.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >= g_FreehandSelection_ResolutionInputLineSquared) g_FreehandSelection_InputLine.push(target); return false; } function positionUnitsFreehandSelectionMouseUp(ev) { inputState = INPUT_NORMAL; let inputLine = g_FreehandSelection_InputLine; g_FreehandSelection_InputLine = []; if (ev.button != SDL_BUTTON_RIGHT) return true; let lengthOfLine = 0; for (let i = 1; i < inputLine.length; ++i) lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]); const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b); // Checking the line for a minimum length to save performance. if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits) { let action = determineAction(ev.x, ev.y); return !!action && doAction(action, ev); } // Even distribution of the units on the line. let p0 = inputLine[0]; let entityDistribution = [p0]; let distanceBetweenEnts = lengthOfLine / (selection.length - 1); let freeDist = -distanceBetweenEnts; for (let i = 1; i < inputLine.length; ++i) { let p1 = inputLine[i]; freeDist += inputLine[i - 1].distanceTo(p1); while (freeDist >= 0) { p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1); entityDistribution.push(p0); freeDist -= distanceBetweenEnts; } } // Rounding errors can lead to missing or too many points. entityDistribution = entityDistribution.slice(0, selection.length); entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1])); if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) + Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) > Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) + Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0])) entityDistribution.reverse(); Engine.PostNetworkCommand({ "type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom", "entities": selection, "targetPositions": entityDistribution.map(pos => pos.toFixed(2)), "targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }, "queued": Engine.HotkeyIsPressed("session.queue"), "pushFront": Engine.HotkeyIsPressed("session.pushorderfront"), "formation": NULL_FORMATION, }); // Add target markers with a minimum distance of 5 to each other. let entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts); for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker) DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; } +function triggerFlareAction(target) +{ + let now = Date.now(); + if (g_LastFlareTime && now < g_LastFlareTime + g_FlareCooldown) + return; + + g_LastFlareTime = now; + displayFlare(target, Engine.GetPlayerID()); + Engine.PlayUISound(g_FlareSound, false); + Engine.PostNetworkCommand({ + "type": "map-flare", + "target": target + }); +} + function handleUnitAction(target, action) { if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute) { error("Invalid action.type " + action.type); return false; } let selection = Engine.HotkeyIsPressed("session.orderone") && popOneFromSelection(action) || g_Selection.toList(); // If the session.queue hotkey is down, add the order to the unit's order queue instead // of running it immediately. If the pushorderfront hotkey is down, execute the order // immidiately and continue the rest of the queue afterwards. return g_UnitActions[action.type].execute( target, action, selection, Engine.HotkeyIsPressed("session.queue"), Engine.HotkeyIsPressed("session.pushorderfront")); } function getEntityLimitAndCount(playerState, entType) { let ret = { "entLimit": undefined, "entCount": undefined, "entLimitChangers": undefined, "canBeAddedCount": undefined, "matchLimit": undefined, "matchCount": undefined, "type": undefined }; if (!playerState.entityLimits) return ret; let template = GetTemplateData(entType); let entCategory; let matchLimit; if (template.trainingRestrictions) { entCategory = template.trainingRestrictions.category; matchLimit = template.trainingRestrictions.matchLimit; ret.type = "training"; } else if (template.buildRestrictions) { entCategory = template.buildRestrictions.category; matchLimit = template.buildRestrictions.matchLimit; ret.type = "build"; } if (entCategory && playerState.entityLimits[entCategory] !== undefined) { ret.entLimit = playerState.entityLimits[entCategory] || 0; ret.entCount = playerState.entityCounts[entCategory] || 0; ret.entLimitChangers = playerState.entityLimitChangers[entCategory]; ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0); } if (matchLimit) { ret.matchLimit = matchLimit; ret.matchCount = playerState.matchEntityCounts[entType] || 0; ret.canBeAddedCount = Math.min(Math.max(ret.entLimit - ret.entCount, 0), Math.max(ret.matchLimit - ret.matchCount, 0)); } return ret; } /** * Called by GUI when user clicks construction button. * @param {string} buildTemplate - Template name of the entity the user wants to build. */ function startBuildingPlacement(buildTemplate, playerState) { if (getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0) return; // TODO: we should clear any highlight selection rings here. If the cursor was over an entity before going onto the GUI // to start building a structure, then the highlight selection rings are kept during the construction of the structure. // Gives the impression that somehow the hovered-over entity has something to do with the structure you're building. placementSupport.Reset(); let templateData = GetTemplateData(buildTemplate); if (templateData.wallSet) { placementSupport.mode = "wall"; placementSupport.wallSet = templateData.wallSet; inputState = INPUT_BUILDING_PLACEMENT; } else { placementSupport.mode = "building"; placementSupport.template = buildTemplate; inputState = INPUT_BUILDING_PLACEMENT; } if (templateData.attack && templateData.attack.Ranged && templateData.attack.Ranged.maxRange) placementSupport.attack = templateData.attack; } // Batch training: // When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING // When the user releases shift, or clicks on a different training button, we create the batched units var g_BatchTrainingEntities; var g_BatchTrainingType; var g_NumberOfBatches; var g_BatchTrainingEntityAllowedCount; var g_BatchSize = getDefaultBatchTrainingSize(); function OnTrainMouseWheel(dir) { if (!Engine.HotkeyIsPressed("session.batchtrain")) return; g_BatchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio"); if (g_BatchSize < 1 || !Number.isFinite(g_BatchSize)) g_BatchSize = 1; updateSelectionDetails(); } function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType) { return entitiesToCheck.filter(entity => { let state = GetEntityState(entity); return state && state.production && state.production.entities.length && state.production.entities.indexOf(trainEntType) != -1 && (!state.upgrade || !state.upgrade.isUpgrading); }); } function initBatchTrain() { registerConfigChangeHandler(changes => { if (changes.has("gui.session.batchtrainingsize")) updateDefaultBatchSize(); }); } function getDefaultBatchTrainingSize() { let num = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize"); return Number.isInteger(num) && num > 0 ? num : 5; } function getBatchTrainingSize() { return Math.max(Math.round(g_BatchSize), 1); } function updateDefaultBatchSize() { g_BatchSize = getDefaultBatchTrainingSize(); } /** * Add the unit shown at position to the training queue for all entities in the selection. * @param {number} position - The position of the template to train. */ function addTrainingByPosition(position) { let playerState = GetSimState().players[Engine.GetPlayerID()]; let selection = g_Selection.toList(); if (!playerState || !selection.length) return; let trainableEnts = getAllTrainableEntitiesFromSelection(); let entToTrain = trainableEnts[position]; if (!entToTrain) return; addTrainingToQueue(selection, entToTrain, playerState); } // Called by GUI when user clicks training button function addTrainingToQueue(selection, trainEntType, playerState) { let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; let decrement = Engine.HotkeyIsPressed("selection.remove"); let template; if (!decrement) template = GetTemplateData(trainEntType); // Batch training only possible if we can train at least 2 units. if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) { if (inputState == INPUT_BATCHTRAINING) { // Check if we are training in the same structure(s) as the last batch. // NOTE: We just check if the arrays are the same and if the order is the same. // If the order changed, we have a new selection and we should create a new batch. // If we're already creating a batch of this unit (in the same structure(s)), then just extend it // (if training limits allow). if (g_BatchTrainingEntities.length == selection.length && g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) && g_BatchTrainingType == trainEntType) { if (decrement) { --g_NumberOfBatches; if (g_NumberOfBatches <= 0) inputState = INPUT_NORMAL; } else if (canBeAddedCount == undefined || canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length) { if (Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize()) })) return; ++g_NumberOfBatches; } g_BatchTrainingEntityAllowedCount = canBeAddedCount; return; } else if (!decrement) flushTrainingBatch(); } if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, getBatchTrainingSize()) })) return; inputState = INPUT_BATCHTRAINING; g_BatchTrainingEntities = selection; g_BatchTrainingType = trainEntType; g_BatchTrainingEntityAllowedCount = canBeAddedCount; g_NumberOfBatches = 1; } else { let buildingsForTraining = appropriateBuildings; if (canBeAddedCount !== undefined) buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); Engine.PostNetworkCommand({ "type": "train", "template": trainEntType, "count": 1, "entities": buildingsForTraining }); } } /** * Returns the number of units that will be present in a batch if the user clicks * the training button depending on the batch training modifier hotkey. */ function getTrainingStatus(selection, trainEntType, playerState) { let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); let nextBatchTrainingCount = 0; let canBeAddedCount; if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType) { nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize(); canBeAddedCount = g_BatchTrainingEntityAllowedCount; } else canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; // We need to calculate count after the next increment if possible. if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) && Engine.HotkeyIsPressed("session.batchtrain")) nextBatchTrainingCount += getBatchTrainingSize(); nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1); // If training limits don't allow us to train batchedSize in each appropriate structure, // train as many full batches as we can and the remainder in one more structure. let buildingsCountToTrainFullBatch = appropriateBuildings.length; let remainderToTrain = 0; if (canBeAddedCount !== undefined && canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length) { buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount); remainderToTrain = canBeAddedCount % nextBatchTrainingCount; } return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; } function flushTrainingBatch() { let batchedSize = g_NumberOfBatches * getBatchTrainingSize(); let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType); // If training limits don't allow us to train batchedSize in each appropriate structure. if (g_BatchTrainingEntityAllowedCount !== undefined && g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length) { // Train as many full batches as we can. let buildingsCountToTrainFullBatch = Math.floor(g_BatchTrainingEntityAllowedCount / batchedSize); Engine.PostNetworkCommand({ "type": "train", "entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch), "template": g_BatchTrainingType, "count": batchedSize }); // Train remainer in one more structure. let remainer = g_BatchTrainingEntityAllowedCount % batchedSize; if (remainer) Engine.PostNetworkCommand({ "type": "train", "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], "template": g_BatchTrainingType, "count": remainer }); } else Engine.PostNetworkCommand({ "type": "train", "entities": appropriateBuildings, "template": g_BatchTrainingType, "count": batchedSize }); } function performGroup(action, groupId) { switch (action) { case "snap": case "select": case "add": let toSelect = []; g_Groups.update(); for (let ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); if (action != "add") g_Selection.reset(); g_Selection.addList(toSelect); if (action == "snap" && toSelect.length) { let entState = GetEntityState(toSelect[0]); let position = entState.position; if (position && entState.visibility != "hidden") Engine.CameraMoveTo(position.x, position.z); } break; case "save": case "breakUp": g_Groups.groups[groupId].reset(); if (action == "save") g_Groups.addEntities(groupId, g_Selection.toList()); updateGroups(); break; } } var lastIdleUnit = 0; var currIdleClassIndex = 0; var lastIdleClasses = []; function resetIdleUnit() { lastIdleUnit = 0; currIdleClassIndex = 0; lastIdleClasses = []; } function findIdleUnit(classes) { let append = Engine.HotkeyIsPressed("selection.add"); let selectall = Engine.HotkeyIsPressed("selection.offscreen"); // Reset the last idle unit, etc., if the selection type has changed. if (selectall || classes.length != lastIdleClasses.length || !classes.every((v, i) => v === lastIdleClasses[i])) resetIdleUnit(); lastIdleClasses = classes; let data = { "viewedPlayer": g_ViewedPlayer, "excludeUnits": append ? g_Selection.toList() : [], // If the current idle class index is not 0, put the class at that index first. "idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex)) }; if (!selectall) { data.limit = 1; data.prevUnit = lastIdleUnit; } let idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); if (!idleUnits.length) { // TODO: display a message to indicate no more idle units, or something Engine.GuiInterfaceCall("PlaySoundForPlayer", { "name": "no_idle_unit" }); resetIdleUnit(); return; } if (!append) g_Selection.reset(); g_Selection.addList(idleUnits); if (selectall) return; lastIdleUnit = idleUnits[0]; let entityState = GetEntityState(lastIdleUnit); if (entityState.position) Engine.CameraMoveTo(entityState.position.x, entityState.position.z); // Move the idle class index to the first class an idle unit was found for. let indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem)); currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length; } function clearSelection() { if (inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING) { inputState = INPUT_NORMAL; placementSupport.Reset(); } else g_Selection.reset(); preSelectedAction = ACTION_NONE; } Index: ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMap.js (revision 25690) +++ ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMap.js (revision 25691) @@ -1,67 +1,91 @@ /** * This class is concerned with handling events occurring when the interacts with the minimap, * except for changing the camera position on leftclick. */ class MiniMap { constructor() { - Engine.GetGUIObjectByName("minimap").onWorldClick = this.onWorldClick.bind(this); - Engine.GetGUIObjectByName("minimap").onMouseEnter = this.onMouseEnter.bind(this); - Engine.GetGUIObjectByName("minimap").onMouseLeave = this.onMouseLeave.bind(this); + this.miniMap = Engine.GetGUIObjectByName("minimap"); + this.miniMap.onWorldClick = this.onWorldClick.bind(this); + this.miniMap.onMouseEnter = this.onMouseEnter.bind(this); + this.miniMap.onMouseLeave = this.onMouseLeave.bind(this); this.mouseIsOverMiniMap = false; } onWorldClick(target, button) { // Partly duplicated from handleInputAfterGui(), but with the input being // world coordinates instead of screen coordinates. + if (inputState == INPUT_NORMAL && controlsPlayer(g_ViewedPlayer) && Engine.HotkeyIsPressed("session.flare")) + { + triggerFlareAction(target); + return true; + } + if (button == SDL_BUTTON_LEFT) { - if (inputState != INPUT_PRESELECTEDACTION || preSelectedAction == ACTION_NONE) + if (inputState != INPUT_FLARE && (inputState != INPUT_PRESELECTEDACTION || preSelectedAction == ACTION_NONE)) return false; } else if (button == SDL_BUTTON_RIGHT) { if (inputState == INPUT_PRESELECTEDACTION) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; return true; } + else if (inputState == INPUT_FLARE) + { + inputState = INPUT_NORMAL; + return true; + } else if (inputState != INPUT_NORMAL) return false; } else return false; if (!controlsPlayer(g_ViewedPlayer)) return false; + if (inputState == INPUT_FLARE && button == SDL_BUTTON_LEFT) + { + triggerFlareAction(target); + inputState = INPUT_NORMAL; + return true; + } + let action = determineAction(undefined, undefined, true); if (!action) return false; if (button == SDL_BUTTON_LEFT && !Engine.HotkeyIsPressed("session.queue") && !Engine.HotkeyIsPressed("session.orderone")) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; } return handleUnitAction(target, action); } onMouseEnter() { this.mouseIsOverMiniMap = true; } onMouseLeave() { this.mouseIsOverMiniMap = false; } isMouseOverMiniMap() { return this.mouseIsOverMiniMap; } + + flare(target, playerID) + { + return this.miniMap.flare({ "x": target.x, "y": target.z }, g_DiplomacyColors.getPlayerColor(playerID)); + } } Index: ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMapFlareButton.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMapFlareButton.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMapFlareButton.js (revision 25691) @@ -0,0 +1,29 @@ +/** + * If the button that this class manages is pressed, an idle unit having one of the given classes is selected. + */ +class MiniMapFlareButton +{ + constructor() + { + this.flareButton = Engine.GetGUIObjectByName("flareButton"); + this.flareButton.onPress = this.onPress.bind(this); + registerHotkeyChangeHandler(this.onHotkeyChange.bind(this)); + } + + onHotkeyChange() + { + this.flareButton.tooltip = + colorizeHotkey("%(hotkey)s" + " ", "session.flare") + + translate(this.Tooltip); + } + + onPress() + { + if (g_IsObserver) + return; + if (inputState == INPUT_NORMAL) + inputState = INPUT_FLARE; + } +} + +MiniMapFlareButton.prototype.Tooltip = markForTranslation("Send a flare to your allies"); Property changes on: ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMapFlareButton.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25690) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25691) @@ -1,2140 +1,2142 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronized 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 = {}; this.templateModified = {}; this.selectionDirty = {}; this.obstructionSnap = new ObstructionSnap(); }; /* * 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 cmpPlayer = QueryPlayerIDInterface(i); let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits); // Work out which phase we are in. let phase = ""; let cmpTechnologyManager = QueryPlayerIDInterface(i, 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"; } 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(), "resourceGatherers": cmpPlayer.GetResourceGatherers(), "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, "matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : 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": cmpPlayer.CanBarter(), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer) }); } 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 = cmpTerrain.GetMapSize(); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); 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; } let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.victoryConditions = cmpEndGameManager.GetVictoryConditions(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, 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() { let ret = this.GetSimulationState(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences(); } return ret; }; /** * Returns the gamesettings that were chosen at the time the match started. */ GuiInterface.prototype.GetInitAttributes = function() { return InitAttributes; }; /** * This data will be stored in the replay metadata file after a match has been finished recording. */ GuiInterface.prototype.GetReplayMetadata = function() { let extendedSimState = this.GetExtendedSimulationState(); return { "timeElapsed": extendedSimState.timeElapsed, "playerStates": extendedSimState.players, "mapSettings": InitAttributes.settings }; }; /** * Called when the game ends if the current game is part of a campaign run. */ GuiInterface.prototype.GetCampaignGameEndData = function(player) { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); if (Trigger.prototype.OnCampaignGameEnd) return Trigger.prototype.OnCampaignGameEnd(); return {}; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); 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); if (!ent) return null; // 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, "player": INVALID_PLAYER, "template": template }; 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(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable(), "hasSomeFormation": cmpIdentity.HasSomeFormation(), "formations": cmpIdentity.GetFormationsList(), "controllable": cmpIdentity.IsControllable() }; let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) ret.position = cmpPosition.GetPosition(); let cmpHealth = QueryMiragedInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = cmpHealth.GetHitpoints(); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured(); ret.needsHeal = !cmpHealth.IsUnhealable(); } 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() }; let cmpPopulation = Engine.QueryInterface(ent, IID_Population); if (cmpPopulation) ret.population = { "bonus": cmpPopulation.GetPopBonus() }; let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades": cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo(), "isUpgrading": cmpUpgrade.IsUpgrading() }; let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver); if (cmpStatusEffects) ret.statusEffects = cmpStatusEffects.GetActiveStatuses(); let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) ret.production = { "entities": cmpProductionQueue.GetEntitiesList(), "technologies": cmpProductionQueue.GetTechnologiesList(), "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(), "queue": cmpProductionQueue.GetQueue(), "autoqueue": cmpProductionQueue.IsAutoQueueing() }; let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { "goods": cmpTrader.GetGoods() }; let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation); if (cmpFoundation) ret.foundation = { "numBuilders": cmpFoundation.GetNumBuilders(), "buildTime": cmpFoundation.GetBuildTime() }; let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable); if (cmpRepairable) ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders(), "buildTime": cmpRepairable.GetBuildTime() }; 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(), "occupiedSlots": cmpGarrisonHolder.OccupiedSlots() }; let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); if (cmpTurretHolder) ret.turretHolder = { "turretPoints": cmpTurretHolder.GetTurretPoints() }; let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (cmpTurretable) ret.turretable = { "ejectable": cmpTurretable.IsEjectable(), "holder": cmpTurretable.HolderID() }; let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable) ret.garrisonable = { "holder": cmpGarrisonable.HolderID(), "size": cmpGarrisonable.UnitSize() }; 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(), "selectableStances": cmpUnitAI.GetSelectableStances(), "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(); ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); } 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 = true; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); 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] = {}; Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type)); ret.attack[type].attackName = cmpAttack.GetAttackName(type); ret.attack[type].splash = cmpAttack.GetSplashData(type); if (ret.attack[type].splash) Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true)); 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; if (cmpPosition && cmpPosition.IsInWorld()) // For units, take the range in front of it, no spread, so angle = 0, // else, take the average elevation around it: angle = 2 * pi. ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI); else // Not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } let cmpResistance = QueryMiragedInterface(ent, IID_Resistance); if (cmpResistance) ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity"); 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() }; if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); 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 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("Barter")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "health": cmpHeal.GetHealth(), "range": cmpHeal.GetRange().max, "interval": cmpHeal.GetInterval(), "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses() }; let cmpLoot = Engine.QueryInterface(ent, IID_Loot); if (cmpLoot) { ret.loot = cmpLoot.GetResources(); ret.loot.xp = cmpLoot.GetXp(); } let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle); if (cmpResourceTrickle) ret.resourceTrickle = { "interval": cmpResourceTrickle.GetInterval(), "rates": cmpResourceTrickle.GetRates() }; let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure); if (cmpTreasure) ret.treasure = { "collectTime": cmpTreasure.CollectionTime(), "resources": cmpTreasure.Resources() }; let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector); if (cmpTreasureCollector) ret.treasureCollector = true; let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier() }; let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep); if (cmpUpkeep) ret.upkeep = { "interval": cmpUpkeep.GetInterval(), "rates": cmpUpkeep.GetRates() }; return ret; }; GuiInterface.prototype.GetMultipleEntityStates = function(player, ents) { return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) })); }; 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, data) { let templateName = data.templateName; let owner = data.player !== undefined ? data.player : player; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(templateName); if (!template) return null; let aurasTemplate = {}; if (!template.Auras) return GetTemplateDataHelper(template, owner, aurasTemplate); let auraNames = template.Auras._string.split(/\s+/); for (let name of auraNames) { let auraTemplate = AuraTemplates.Get(name); if (!auraTemplate) error("Template " + templateName + " has undefined aura " + name); else aurasTemplate[name] = auraTemplate; } return GetTemplateDataHelper(template, owner, aurasTemplate); }; GuiInterface.prototype.IsTechnologyResearched = function(player, data) { if (!data.tech) return true; let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? 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 !== undefined ? 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 of 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; ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining; } else { ret[tech].progress = 0; ret[tech].timeRemaining = 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) { let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection); if (!cmpAttackDetection) return []; return cmpAttackDetection.GetIncomingAttacks(); }; /** * Used to show a red square over GUI elements you can't yet afford. */ GuiInterface.prototype.GetNeededResources = function(player, data) { let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player); return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {}; }; /** * State of the templateData (player dependent): true when some template values have been modified * and need to be reloaded by the gui. */ GuiInterface.prototype.OnTemplateModification = function(msg) { this.templateModified[msg.player] = true; this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.IsTemplateModified = function(player) { return this.templateModified[player] || false; }; GuiInterface.prototype.ResetTemplateModified = function() { this.templateModified = {}; }; /** * Some changes may require an update to the selection panel, * which is cached for efficiency. Inform the GUI it needs reloading. */ GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.SetSelectionDirty = function(player) { this.selectionDirty[player] = true; }; GuiInterface.prototype.IsSelectionDirty = function(player) { return this.selectionDirty[player] || false; }; GuiInterface.prototype.ResetSelectionDirty = function() { this.selectionDirty = {}; }; /** * 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) { notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); notification.players[0] = -1; } 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) { let cmpPlayer = QueryPlayerIDInterface(wantedPlayer); if (!cmpPlayer) return []; return cmpPlayer.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) { return data.ents.some(ent => { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate; }); }; 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.UpdateDisplayedPlayerColors = function(player, data) { let updateEntityColor = (iids, entities) => { for (let ent of entities) for (let iid of iids) { let cmp = Engine.QueryInterface(ent, iid); if (cmp) cmp.UpdateColor(); } }; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i, IID_Player); if (!cmpPlayer) continue; cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors); if (data.displayDiplomacyColors) cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]); updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ? [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] : [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer], cmpRangeManager.GetEntitiesByPlayer(i)); } updateEntityColor([IID_Selectable, IID_StatusBars], data.selected); Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors(); }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { // Cache of owner -> color map let playerColors = {}; 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 = INVALID_PLAYER; 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.GetDisplayedColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER) continue; cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false); } }; GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data) { this.enabledVisualRangeOverlayTypes[data.type] = data.enabled; }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return Array.from(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, cmd.showRank, cmd.showExperience); 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 cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.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 // May return undefined if no rally point is set. pos = cmpRallyPoint.GetPositions()[0]; if (pos) { // Only update the position if we changed it (cmd.queued is set). // Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z. if ("queued" in cmd) { if (cmd.queued == true) cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z)); else cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z)); } else if (!cmpRallyPointRenderer.IsSet()) // Rebuild the renderer when not set (when reading saved game or in case of building update). for (let posi of cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z)); cmpRallyPointRenderer.SetDisplayed(true); // Remember which entities have their rally points displayed so we can hide them again. this.entsRallyPointsDisplayed.push(ent); } } }; GuiInterface.prototype.AddTargetMarker = function(player, cmd) { let ent = Engine.AddLocalEntity(cmd.template); if (!ent) return; - + let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); + if (cmpOwnership) + cmpOwnership.SetOwner(cmd.owner); let cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); }; /** * 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": [] }; if (!this.placementEntity || this.placementEntity[0] != cmd.template) { if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); if (cmd.template == "") this.placementEntity = undefined; else this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } if (this.placementEntity) { let ent = this.placementEntity[1]; 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); let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); // 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': ..., * } * } * * @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; // Did the start position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let start = { "pos": cmd.start, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // Did the end position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let end = { "pos": cmd.end, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // -------------------------------------------------------------------------------- // 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; } 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) { if (type == "curves") continue; let tpl = wallSet.templates[type]; if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, { "templateName": tpl }), }; 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) { // Value of 0.5 was determined through trial and error. let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; 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, "time": 0 } }; for (let res of Resources.GetCodes()) result.cost[res] = 0; let previewEntities = []; if (end.pos) // See helpers/Walls.js. previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // 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 && 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 ? 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 ? 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; // Number of entities that are required to build the entire wall, regardless of validity. let numRequiredPieces = 0; 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) { ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else 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); 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", "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 (data.snapToEdges) { let position = this.obstructionSnap.getPosition(data, template); if (position) return position; } if (template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return { "x": data.x, "z": data.z, "angle": angle }; } return false; }; GuiInterface.prototype.PlaySoundForPlayer = function(player, data) { let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player); let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound); if (!cmpSound) return; let soundGroup = cmpSound.GetSoundGroup(data.name); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player); }; 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. let 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()) return { "idle": false }; let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable); if (cmpGarrisonable && cmpGarrisonable.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; let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market); return cmpMarket && cmpMarket.CalculateTraderGain(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 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) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return []; return cmpPlayer.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, "GetInitAttributes": 1, "GetReplayMetadata": 1, "GetCampaignGameEndData": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetMultipleEntityStates": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 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, "UpdateDisplayedPlayerColors": 1, "SetSelectionHighlight": 1, "GetAllBuildableEntities": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "GetNonGaiaEntities": 1, "DisplayRallyPoint": 1, "AddTargetMarker": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "PlaySoundForPlayer": 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, "IsTemplateModified": 1, "ResetTemplateModified": 1, "IsSelectionDirty": 1, "ResetSelectionDirty": 1 }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); throw new Error("Invalid GuiInterface Call name \"" + name + "\""); }; Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/source/gui/GUIObjectTypes.cpp =================================================================== --- ps/trunk/source/gui/GUIObjectTypes.cpp (revision 25690) +++ ps/trunk/source/gui/GUIObjectTypes.cpp (revision 25691) @@ -1,61 +1,62 @@ /* Copyright (C) 2020 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 "gui/ObjectTypes/CButton.h" #include "gui/ObjectTypes/CChart.h" #include "gui/ObjectTypes/CCheckBox.h" #include "gui/ObjectTypes/CDropDown.h" #include "gui/ObjectTypes/CGUIDummyObject.h" #include "gui/ObjectTypes/CHotkeyPicker.h" #include "gui/ObjectTypes/CImage.h" #include "gui/ObjectTypes/CInput.h" #include "gui/ObjectTypes/CList.h" #include "gui/ObjectTypes/CMiniMap.h" #include "gui/ObjectTypes/COList.h" #include "gui/ObjectTypes/CProgressBar.h" #include "gui/ObjectTypes/CRadioButton.h" #include "gui/ObjectTypes/CSlider.h" #include "gui/ObjectTypes/CText.h" #include "gui/ObjectTypes/CTooltip.h" #include "gui/Scripting/JSInterface_GUIProxy.h" void CGUI::AddObjectTypes() { m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface)); m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface)); m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface)); + m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface)); m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface)); AddObjectType("button", &CButton::ConstructObject); AddObjectType("chart", &CChart::ConstructObject); AddObjectType("checkbox", &CCheckBox::ConstructObject); AddObjectType("dropdown", &CDropDown::ConstructObject); AddObjectType("empty", &CGUIDummyObject::ConstructObject); AddObjectType("hotkeypicker", &CHotkeyPicker::ConstructObject); AddObjectType("image", &CImage::ConstructObject); AddObjectType("input", &CInput::ConstructObject); AddObjectType("list", &CList::ConstructObject); AddObjectType("minimap", &CMiniMap::ConstructObject); AddObjectType("olist", &COList::ConstructObject); AddObjectType("progressbar", &CProgressBar::ConstructObject); AddObjectType("radiobutton", &CRadioButton::ConstructObject); AddObjectType("slider", &CSlider::ConstructObject); AddObjectType("text", &CText::ConstructObject); AddObjectType("tooltip", &CTooltip::ConstructObject); } Index: ps/trunk/source/gui/Scripting/JSInterface_GUIProxy.cpp =================================================================== --- ps/trunk/source/gui/Scripting/JSInterface_GUIProxy.cpp (revision 25690) +++ ps/trunk/source/gui/Scripting/JSInterface_GUIProxy.cpp (revision 25691) @@ -1,60 +1,68 @@ /* Copyright (C) 2021 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 "JSInterface_GUIProxy_impl.h" #include "gui/ObjectBases/IGUIObject.h" #include "gui/ObjectTypes/CButton.h" #include "gui/ObjectTypes/CList.h" +#include "gui/ObjectTypes/CMiniMap.h" #include "gui/ObjectTypes/CText.h" // Called for every specialization - adds the common interface. template<> void JSI_GUIProxy::CreateFunctions(const ScriptRequest& rq, GUIProxyProps* cache) { CreateFunction<&IGUIObject::GetName>(rq, cache, "toString"); CreateFunction<&IGUIObject::GetName>(rq, cache, "toSource"); CreateFunction<&IGUIObject::SetFocus>(rq, cache, "focus"); CreateFunction<&IGUIObject::ReleaseFocus>(rq, cache, "blur"); CreateFunction<&IGUIObject::GetComputedSize>(rq, cache, "getComputedSize"); } DECLARE_GUIPROXY(IGUIObject); // Implement derived types below. // CButton template<> void JSI_GUIProxy::CreateFunctions(const ScriptRequest& rq, GUIProxyProps* cache) { CreateFunction<&CButton::GetTextSize>(rq, cache, "getTextSize"); } DECLARE_GUIPROXY(CButton); // CText template<> void JSI_GUIProxy::CreateFunctions(const ScriptRequest& rq, GUIProxyProps* cache) { CreateFunction<&CText::GetTextSize>(rq, cache, "getTextSize"); } DECLARE_GUIPROXY(CText); // CList template<> void JSI_GUIProxy::CreateFunctions(const ScriptRequest& rq, GUIProxyProps* cache) { CreateFunction(&CList::AddItem)>(rq, cache, "addItem"); } DECLARE_GUIPROXY(CList); + +// CMiniMap +template<> void JSI_GUIProxy::CreateFunctions(const ScriptRequest& rq, GUIProxyProps* cache) +{ + CreateFunction<&CMiniMap::Flare>(rq, cache, "flare"); +} +DECLARE_GUIPROXY(CMiniMap); Index: ps/trunk/binaries/data/mods/public/art/animation/other/flare_target_marker_idle.dae =================================================================== --- ps/trunk/binaries/data/mods/public/art/animation/other/flare_target_marker_idle.dae (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/animation/other/flare_target_marker_idle.dae (revision 25691) @@ -0,0 +1,313 @@ + + + + + Blender User + Blender 2.90.1 commit date:2020-09-23, commit time:06:43, hash:3e85bb34d0d7 + + 2021-04-22T18:38:40 + 2021-04-22T18:38:40 + + Z_UP + + + + + + + + 0 0 0 1 + + + 0.8 0.8 0.8 1 + + + 0.5 + + + + + + + + + + + 0 0 0 1 + + + 0.7679928 0 0.8000001 1 + + + 1.45 + + + + + + + + + + + + + + + + + + + + -1.2542e-5 0.16854 0.876138 -1.2542e-5 0.2919237 0.7460136 -1.2542e-5 0.3370852 0.5682601 -1.2542e-5 0.2919237 0.3905067 -1.2542e-5 0.16854 0.2603824 -1.25861e-5 -5.1786e-6 0.9237671 0.1459518 0.08426737 0.876138 0.2528052 0.1459592 0.7460136 0.2919163 0.16854 0.5682601 0.2528052 0.1459592 0.3905067 0.1459519 0.08426737 0.2603824 0.1459518 -0.08427774 0.876138 0.2528052 -0.1459696 0.7460136 0.2919163 -0.1685503 0.5682601 0.2528052 -0.1459696 0.3905067 0.1459518 -0.08427774 0.2603824 -1.2571e-5 -0.1685503 0.876138 -1.25861e-5 -0.291934 0.7460136 -1.25861e-5 -0.3370956 0.5682601 -1.2566e-5 -0.291934 0.3905067 -1.25761e-5 -0.1685503 0.2603824 -1.2542e-5 -5.15315e-6 0.2127533 -0.1459769 -0.08427768 0.876138 -0.2528303 -0.1459695 0.7460136 -0.2919414 -0.1685503 0.5682601 -0.2528303 -0.1459695 0.3905067 -0.145977 -0.08427768 0.2603824 -0.1459769 0.08426743 0.876138 -0.2528303 0.1459593 0.7460136 -0.2919414 0.1685401 0.5682601 -0.2528303 0.1459593 0.3905067 -0.1459769 0.08426749 0.2603824 -0.5031438 0.4067856 3.926643 -0.4191778 0.416755 4.019573 -0.4825602 0.4137057 3.992354 -0.4690158 0.4665592 3.980295 -0.4161745 0.4801436 3.992353 -0.4755474 0.4730942 3.926642 -0.4191778 -0.4167107 4.019573 -0.4755485 -0.4729414 3.926643 -0.4690163 -0.4664185 3.980296 -0.4161757 -0.4801049 3.992354 -0.4825598 -0.4135391 3.992355 0.4069955 -0.5029647 3.926642 0.473306 -0.4755504 3.926643 0.4667804 -0.4689928 3.980296 0.4803683 -0.4161868 3.992355 0.4139276 -0.4823904 3.992354 0.5016148 0.4075928 3.926643 0.4078046 0.5014013 3.926642 0.4176568 0.4174291 4.019573 0.4740707 0.4738581 3.926643 0.4675345 0.4673132 3.980296 0.4147034 0.4808134 3.992354 0.4810373 0.4144808 3.992355 -0.1518459 0.1500419 1.388505 -0.2271001 0.2252219 1.481435 -0.2096655 0.2078092 1.427782 -0.154374 0.218949 1.415724 -0.2207594 0.1525109 1.415723 -0.1518459 -0.1505176 1.388505 -0.220759 -0.1528535 1.415723 -0.209666 -0.208173 1.427782 -0.1543751 -0.2194195 1.415724 -0.2271012 -0.2255524 1.481435 0.1587484 -0.254209 1.481436 0.1498667 -0.1513479 1.388505 0.2527096 -0.1605827 1.481434 0.1523379 -0.2202646 1.415724 0.207639 -0.2093205 1.427782 0.2187784 -0.1540609 1.415723 0.2250588 -0.2267946 1.481435 0.2529672 0.15932 1.481434 0.1501096 0.1502852 1.388505 0.1591573 0.2531288 1.481436 0.2190256 0.1528641 1.415723 0.2079753 0.2081452 1.427782 0.152692 0.2191969 1.415724 0.2254233 0.2255855 1.481435 -0.1608152 -0.2533295 1.481436 -0.4092624 -0.5007183 3.926642 0.4169831 -0.4190118 4.019573 -0.5031437 -0.40658 3.926643 -0.2546963 0.1589133 1.481434 -0.2546963 -0.1591908 1.481434 -0.4529223 0.4067856 3.926643 -0.4529222 -0.40658 3.926643 -0.2293195 -0.1591908 1.481434 -0.2293195 0.1589133 1.481434 -0.4092624 -0.4506849 3.926642 0.4069955 -0.4527066 3.926642 0.1587484 -0.2288265 1.481436 -0.1608152 -0.228035 1.481436 0.5009569 -0.4093387 3.926643 0.4507682 -0.4093387 3.926643 0.4513604 0.4075928 3.926643 0.2275776 0.15932 1.481434 0.2273457 -0.1605827 1.481434 -0.4092603 0.500743 3.926642 -0.160813 0.252871 1.481436 0.4078046 0.4512226 3.926642 -0.4092603 0.4506303 3.926642 -0.160813 0.2275454 1.481436 0.1591573 0.2277774 1.481436 -0.4191778 -0.4167107 3.969573 -0.4191778 0.416755 3.969573 0.4169831 -0.4190118 3.969573 0.4176568 0.4174291 3.969573 -0.4529223 0.4067856 3.926643 -0.4529222 -0.40658 3.926643 -0.2293195 -0.1591908 1.481434 -0.2293195 0.1589133 1.481434 -0.4092624 -0.4506849 3.926642 0.4069955 -0.4527066 3.926642 0.1587484 -0.2288265 1.481436 -0.1608152 -0.228035 1.481436 0.4507682 -0.4093387 3.926643 0.4513604 0.4075928 3.926643 0.2275776 0.15932 1.481434 0.2273457 -0.1605827 1.481434 0.4078046 0.4512226 3.926642 -0.4092603 0.4506303 3.926642 -0.160813 0.2275454 1.481436 0.1591573 0.2277774 1.481436 -0.4191778 -0.4167107 3.969573 -0.4191778 0.416755 3.969573 0.4169831 -0.4190118 3.969573 0.4176568 0.4174291 3.969573 + + + + + + + + + + 0 0 -1 0 0.5755252 -0.8177841 0.4984077 0.2877648 -0.8177904 0 1 0 0.7727485 0.446131 -0.4514719 0 0.8922869 -0.451469 0 0.5755252 0.8177841 0.7727485 0.446131 0.4514719 0 0.8922869 0.451469 0.8660317 0.4999893 0 0 0 1 0.4984077 0.2877648 0.8177904 0.7727485 -0.446131 -0.4514719 0.4984077 -0.2877648 -0.8177904 0.8660317 -0.4999893 0 0.4984077 -0.2877648 0.8177904 0.7727485 -0.446131 0.4514719 0 -0.5755252 -0.8177841 0 -1 0 0 -0.5755252 0.8177841 0 -0.8922869 -0.451469 0 -0.8922869 0.451469 -0.7727485 -0.446131 -0.4514719 -0.4984077 -0.2877648 -0.8177904 -0.8660317 -0.4999893 0 -0.4984077 -0.2877648 0.8177904 -0.7727485 -0.446131 0.4514719 -0.4984077 0.2877648 -0.8177904 -0.8660317 0.4999893 0 -0.4984077 0.2877648 0.8177904 -0.7727485 0.446131 -0.4514719 -0.7727485 0.446131 0.4514719 0.169104 0.169104 -0.9709829 -0.1695967 -0.1691694 -0.9708856 -0.169319 0.1691969 -0.9709292 -0.9774976 -0.08664381 -0.1923316 -0.6105381 0.6100192 0.505094 -0.7073171 0.7067678 0.01348954 0.1089839 -0.1089228 0.988058 -0.7393861 0.1604999 0.6538715 0.08594143 0.9775537 -0.1923612 -0.1610834 0.7392194 0.6539167 -0.977478 0.08685845 -0.1923338 -0.6111707 -0.6094921 0.5049651 -0.7395038 -0.1603463 0.6537762 0.08432459 -0.9775921 -0.1928815 -0.7080187 -0.7060655 0.01345902 0.1093208 0.1090461 0.9880071 -0.1623929 -0.7390237 0.653814 -0.08896237 -0.9774574 -0.1914752 0.6091147 -0.6112816 0.5052863 0.1586377 -0.7396278 0.6540526 0.977559 0.08560621 -0.1924843 0.7057749 -0.7083079 0.01348924 -0.1084657 0.108832 0.9881249 0.7390979 -0.1612666 0.6540088 0.9774962 -0.08713203 -0.1921177 0.6102028 0.6102028 0.5052775 0.7393777 0.1600708 0.6539862 -0.08719253 0.977521 -0.191964 0.7070425 0.7070425 0.01348954 -0.1086786 -0.1086786 0.9881184 0.1600451 0.7393389 0.6540364 -0.9862205 -0.1552805 0.05707043 -0.5430032 0.5426064 -0.6408792 -0.6592218 0.136819 -0.7393966 0.1544885 0.9863219 0.05746775 -0.6803995 0.6798502 -0.2736064 -0.1373367 0.6591553 -0.73936 -0.9861934 0.1554641 0.05704009 -0.5436445 -0.5421796 -0.6406966 -0.6810982 -0.6792365 -0.2733915 -0.659361 -0.1367247 -0.7392899 0.1528075 -0.9865498 0.05804669 -0.1385578 -0.6590347 -0.7392395 -0.1576307 -0.9858863 0.0563991 0.5416525 -0.5436056 -0.6411751 0.6788417 -0.6812832 -0.2739111 0.1687701 -0.1693805 -0.9709928 0.1351404 -0.6594657 -0.7394878 0.9863629 0.1542718 0.05734455 0.6589313 -0.1374872 -0.7395315 0.9861475 -0.1558328 0.05682736 0.5426312 0.5426617 -0.6411471 0.680055 0.6800855 -0.2738776 0.6591807 0.1364201 -0.7395069 -0.1558014 0.9861419 0.05701017 0.1363905 0.6592156 -0.7394812 -0.9948861 0 0.1010045 0.9948778 0 0.1010854 3.16847e-7 0 1 1 0 0 -0.9999998 8.05407e-4 0 0.002752006 0.9999963 0 8.05503e-4 -0.9999997 0 0 -0.9949013 0.1008538 0 0.994921 0.1006592 0 -0.994885 0.1010152 0 0.9948652 0.1012097 -0.9948696 0 0.101166 -2.04709e-7 0 -1 0.9948779 0 0.1010853 -9.185e-7 0 1 -0.002466559 -0.9958544 -0.09092879 -7.9896e-7 0 1 -0.995845 0 -0.09106522 0.9958447 -7.2194e-4 -0.09106528 -7.22026e-4 0.9958571 -0.09092891 0 0 -1 -0.994886 0 0.1010048 0.9948779 0 0.101085 -9.18544e-7 0 1 -0.9999997 8.05336e-4 0 0.002751946 0.9999963 0 8.05492e-4 -0.9999997 0 0 -0.9949013 0.100854 0 0.994921 0.1006591 0 -0.994885 0.1010149 0 0.9948652 0.1012096 -0.9948696 0 0.101166 0.9948778 0 0.1010854 1.58028e-7 0 1 -0.002466559 -0.9958544 -0.09092879 -0.995845 0 -0.09106528 0.9958447 -7.21768e-4 -0.09106528 -7.22017e-4 0.9958571 -0.09092891 + + + + + + + + + + 0.2689294 0.9634857 0.2468196 0.9324201 0.2832639 0.9281525 0.2347516 0.8773799 0.2769502 0.895384 0.2382609 0.9014936 0.2276332 0.8173105 0.2720444 0.8479661 0.2324163 0.8524765 0.2382609 0.9014936 0.2832639 0.9281525 0.2468196 0.9324201 0.2324163 0.8524765 0.2744972 0.871675 0.2347516 0.8773799 0.2276332 0.8173105 0.244757 0.7815487 0.2657305 0.8151974 0.2832639 0.9281525 0.316578 0.8908735 0.3213611 0.9260395 0.2720444 0.8479661 0.3142428 0.8659701 0.2744972 0.871675 0.2657305 0.8151974 0.2800651 0.7798643 0.3021747 0.81093 0.3042375 0.9618013 0.2832639 0.9281525 0.3213611 0.9260395 0.2744972 0.871675 0.316578 0.8908735 0.2769502 0.895384 0.2657305 0.8151974 0.3107335 0.8418564 0.2720444 0.8479661 0.316578 0.8908735 0.3617075 0.9264094 0.3213611 0.9260395 0.3107335 0.8418564 0.3581165 0.8580409 0.3142428 0.8659701 0.3021747 0.81093 0.3117759 0.774842 0.337064 0.8023198 0.3411974 0.9629787 0.3213611 0.9260395 0.3617075 0.9264094 0.3142428 0.8659701 0.3607792 0.8871456 0.316578 0.8908735 0.3107335 0.8418564 0.337064 0.8023198 0.3511369 0.8312228 0.3617075 0.9264094 0.4242356 0.8883687 0.4047687 0.9338378 0.3511369 0.8312228 0.4212717 0.8423786 0.3581165 0.8580409 0.337064 0.8023198 0.334192 0.764118 0.3679613 0.7822087 0.3764712 0.9693508 0.3617075 0.9264094 0.4047687 0.9338378 0.3607792 0.8871456 0.4212717 0.8423786 0.4242356 0.8883687 0.337064 0.8023198 0.3993613 0.8057134 0.3511369 0.8312228 0.1496332 0.9376367 0.2119304 0.9410302 0.1810331 0.9611415 0.1247587 0.8549814 0.1908779 0.8853091 0.1277227 0.9009717 0.1442257 0.8095122 0.1725232 0.7739992 0.1872869 0.8169406 0.2148024 0.979232 0.1810331 0.9611415 0.2119304 0.9410302 0.1496332 0.9376367 0.1908779 0.8853091 0.1978574 0.9121273 0.1247587 0.8549814 0.1872869 0.8169406 0.1882152 0.8562045 0.1882152 0.8562045 0.2347516 0.8773799 0.1908779 0.8853091 0.1872869 0.8169406 0.2077969 0.7803713 0.2276332 0.8173105 0.2372184 0.9685081 0.2119304 0.9410302 0.2468196 0.9324201 0.1908779 0.8853091 0.2382609 0.9014936 0.1978574 0.9121273 0.1872869 0.8169406 0.2324163 0.8524765 0.1882152 0.8562045 0.2119304 0.9410302 0.2382609 0.9014936 0.2468196 0.9324201 0.5653417 0.1110531 0.6179833 0.05785548 0.6186115 0.1109533 0.5702925 0.2408316 0.5639945 0.2571572 0.5562849 0.2494693 0.5742035 0.2677037 0.5639945 0.2571572 0.5734679 0.2546974 0.5471693 0.2637142 0.5639945 0.2571572 0.5618067 0.2668063 0.7364018 0.2361788 0.7431017 0.2523872 0.7339112 0.2504816 0.7597498 0.2586472 0.7431017 0.2523872 0.7506223 0.2446186 0.7335389 0.2620936 0.7431017 0.2523872 0.7452447 0.2615504 0.759702 0.4320898 0.7427436 0.4390336 0.7451931 0.428944 0.7362783 0.4560214 0.7427436 0.4390336 0.7507525 0.446845 0.7314437 0.4281437 0.7427436 0.4390336 0.732751 0.4417741 0.5666657 0.4610032 0.5597555 0.4440929 0.5695233 0.446253 0.5429396 0.4374279 0.5597555 0.4440929 0.5519188 0.4520102 0.5704158 0.434005 0.5597555 0.4440929 0.5573531 0.4344296 0.646318 0.1167042 0.6300215 0.1224703 0.6319586 0.1121207 0.6240896 0.1390262 0.6300215 0.1224703 0.6384375 0.1309151 0.6186115 0.1109533 0.6300215 0.1224703 0.6196037 0.1242463 0.6463179 0.05206131 0.6294717 0.04620414 0.6380097 0.03752434 0.6179833 0.05785548 0.6294717 0.04620414 0.6313902 0.05672484 0.6233487 0.02949005 0.6294717 0.04620414 0.6189266 0.04444664 0.5587999 0.03054726 0.55329 0.04660743 0.5450069 0.03846722 0.5647277 0.0578795 0.55329 0.04660743 0.563474 0.04464906 0.537357 0.05238252 0.55329 0.04660743 0.5514918 0.05681538 0.537395 0.1165966 0.5538397 0.1224506 0.54545 0.1308104 0.5653417 0.1110531 0.5538397 0.1224506 0.5520683 0.1120904 0.5595292 0.1386324 0.5538397 0.1224506 0.564132 0.1243813 0.7364018 0.2361788 0.5734679 0.2546974 0.5702925 0.2408316 0.7339112 0.2504816 0.5742035 0.2677037 0.5734679 0.2546974 0.7597498 0.2586472 0.7451931 0.428944 0.7452447 0.2615504 0.7452447 0.2615504 0.7314437 0.4281437 0.7335389 0.2620936 0.5666657 0.4610032 0.732751 0.4417741 0.7362783 0.4560214 0.5695233 0.446253 0.7314437 0.4281437 0.732751 0.4417741 0.5471693 0.2637142 0.5573531 0.4344296 0.5429396 0.4374279 0.5618067 0.2668063 0.5704158 0.434005 0.5573531 0.4344296 0.6186115 0.1109533 0.6313902 0.05672484 0.6319586 0.1121207 0.6319586 0.1121207 0.6463179 0.05206131 0.646318 0.1167042 0.5647277 0.0578795 0.6189266 0.04444664 0.6179833 0.05785548 0.563474 0.04464906 0.6233487 0.02949005 0.6189266 0.04444664 0.5647277 0.0578795 0.5520683 0.1120904 0.5514918 0.05681538 0.5514918 0.05681538 0.537395 0.1165966 0.537357 0.05238252 0.5653417 0.1110531 0.6196037 0.1242463 0.564132 0.1243813 0.564132 0.1243813 0.6240896 0.1390262 0.5595292 0.1386324 0.920629 0.6707581 0.9347499 0.2272918 0.9335129 0.6714929 0.9347499 0.2272918 0.9463997 0.670827 0.9335129 0.6714929 0.5670242 0.965082 0.555373 0.5215635 0.5682703 0.5209023 0.555373 0.5215635 0.5412353 0.9650067 0.5424789 0.5208275 0.6337562 0.9649829 0.6478716 0.5215623 0.6466588 0.9657142 0.6478716 0.5215623 0.6595652 0.9650505 0.6466588 0.9657142 0.8538994 0.670836 0.8422524 0.2272896 0.8551378 0.2266262 0.8422524 0.2272896 0.8281325 0.6707614 0.8293721 0.2265521 0.7597498 0.2586472 0.7703812 0.4320634 0.759702 0.4320898 0.8293721 0.2265521 0.8191422 0.6698262 0.8216365 0.22605 0.6607772 0.5208992 0.6685189 0.9641739 0.6595652 0.9650505 0.6233487 0.02949005 0.5587301 0.02542001 0.6232791 0.02438056 0.7335389 0.2620936 0.5865367 0.2754362 0.5742035 0.2677037 0.5704158 0.434005 0.7223314 0.4201838 0.7314437 0.4281437 0.7314437 0.4281437 0.7237446 0.2732378 0.7335389 0.2620936 0.5742035 0.2677037 0.5785265 0.4229285 0.5704158 0.434005 0.7364018 0.2361788 0.5700052 0.2305752 0.7361146 0.2259224 0.5682703 0.5209023 0.5760124 0.9641988 0.5670242 0.965082 0.6463179 0.05206131 0.6514748 0.1167042 0.646318 0.1167042 0.6349689 0.520831 0.6247736 0.9640501 0.6272342 0.5203303 0.9218659 0.2265568 0.9116415 0.6698235 0.9141306 0.2260552 0.7362783 0.4560214 0.5669806 0.4714367 0.5666657 0.4610032 0.8551378 0.2266262 0.8628804 0.6699542 0.8538994 0.670836 0.537357 0.05238252 0.5322986 0.1165956 0.5322657 0.05238139 0.9476367 0.2266263 0.9553791 0.6699452 0.9463997 0.670827 0.5471693 0.2637142 0.5322717 0.4371595 0.5365153 0.2634462 0.5424789 0.5208275 0.5322717 0.9640774 0.5347437 0.5203289 0.6240896 0.1390262 0.5595021 0.1437476 0.5595292 0.1386324 0.02438068 0.4274392 0.4835041 0.533547 0.02438056 0.5800693 0.9756194 0.8766642 0.8192679 0.7205649 0.9756194 0.7202605 0.4835104 0.6288304 0.02438056 0.7345703 0.02438056 0.6750887 0.02438056 0.378678 0.4835105 0.2724065 0.4835104 0.3322243 0.02438056 0.1771612 0.4835045 0.07090485 0.4835045 0.1307352 0.2347516 0.8773799 0.2744972 0.871675 0.2769502 0.895384 0.2276332 0.8173105 0.2657305 0.8151974 0.2720444 0.8479661 0.2382609 0.9014936 0.2769502 0.895384 0.2832639 0.9281525 0.2324163 0.8524765 0.2720444 0.8479661 0.2744972 0.871675 0.2832639 0.9281525 0.2769502 0.895384 0.316578 0.8908735 0.2720444 0.8479661 0.3107335 0.8418564 0.3142428 0.8659701 0.2744972 0.871675 0.3142428 0.8659701 0.316578 0.8908735 0.2657305 0.8151974 0.3021747 0.81093 0.3107335 0.8418564 0.316578 0.8908735 0.3607792 0.8871456 0.3617075 0.9264094 0.3107335 0.8418564 0.3511369 0.8312228 0.3581165 0.8580409 0.3142428 0.8659701 0.3581165 0.8580409 0.3607792 0.8871456 0.3107335 0.8418564 0.3021747 0.81093 0.337064 0.8023198 0.3617075 0.9264094 0.3607792 0.8871456 0.4242356 0.8883687 0.3511369 0.8312228 0.3993613 0.8057134 0.4212717 0.8423786 0.3607792 0.8871456 0.3581165 0.8580409 0.4212717 0.8423786 0.337064 0.8023198 0.3679613 0.7822087 0.3993613 0.8057134 0.1496332 0.9376367 0.1978574 0.9121273 0.2119304 0.9410302 0.1247587 0.8549814 0.1882152 0.8562045 0.1908779 0.8853091 0.1496332 0.9376367 0.1277227 0.9009717 0.1908779 0.8853091 0.1247587 0.8549814 0.1442257 0.8095122 0.1872869 0.8169406 0.1882152 0.8562045 0.2324163 0.8524765 0.2347516 0.8773799 0.1908779 0.8853091 0.2347516 0.8773799 0.2382609 0.9014936 0.1872869 0.8169406 0.2276332 0.8173105 0.2324163 0.8524765 0.2119304 0.9410302 0.1978574 0.9121273 0.2382609 0.9014936 0.5653417 0.1110531 0.5647277 0.0578795 0.6179833 0.05785548 0.5702925 0.2408316 0.5734679 0.2546974 0.5639945 0.2571572 0.5742035 0.2677037 0.5618067 0.2668063 0.5639945 0.2571572 0.5471693 0.2637142 0.5562849 0.2494693 0.5639945 0.2571572 0.7364018 0.2361788 0.7506223 0.2446186 0.7431017 0.2523872 0.7597498 0.2586472 0.7452447 0.2615504 0.7431017 0.2523872 0.7335389 0.2620936 0.7339112 0.2504816 0.7431017 0.2523872 0.759702 0.4320898 0.7507525 0.446845 0.7427436 0.4390336 0.7362783 0.4560214 0.732751 0.4417741 0.7427436 0.4390336 0.7314437 0.4281437 0.7451931 0.428944 0.7427436 0.4390336 0.5666657 0.4610032 0.5519188 0.4520102 0.5597555 0.4440929 0.5429396 0.4374279 0.5573531 0.4344296 0.5597555 0.4440929 0.5704158 0.434005 0.5695233 0.446253 0.5597555 0.4440929 0.646318 0.1167042 0.6384375 0.1309151 0.6300215 0.1224703 0.6240896 0.1390262 0.6196037 0.1242463 0.6300215 0.1224703 0.6186115 0.1109533 0.6319586 0.1121207 0.6300215 0.1224703 0.6463179 0.05206131 0.6313902 0.05672484 0.6294717 0.04620414 0.6179833 0.05785548 0.6189266 0.04444664 0.6294717 0.04620414 0.6233487 0.02949005 0.6380097 0.03752434 0.6294717 0.04620414 0.5587999 0.03054726 0.563474 0.04464906 0.55329 0.04660743 0.5647277 0.0578795 0.5514918 0.05681538 0.55329 0.04660743 0.537357 0.05238252 0.5450069 0.03846722 0.55329 0.04660743 0.537395 0.1165966 0.5520683 0.1120904 0.5538397 0.1224506 0.5653417 0.1110531 0.564132 0.1243813 0.5538397 0.1224506 0.5595292 0.1386324 0.54545 0.1308104 0.5538397 0.1224506 0.7364018 0.2361788 0.7339112 0.2504816 0.5734679 0.2546974 0.7339112 0.2504816 0.7335389 0.2620936 0.5742035 0.2677037 0.7597498 0.2586472 0.759702 0.4320898 0.7451931 0.428944 0.7452447 0.2615504 0.7451931 0.428944 0.7314437 0.4281437 0.5666657 0.4610032 0.5695233 0.446253 0.732751 0.4417741 0.5695233 0.446253 0.5704158 0.434005 0.7314437 0.4281437 0.5471693 0.2637142 0.5618067 0.2668063 0.5573531 0.4344296 0.5618067 0.2668063 0.5742035 0.2677037 0.5704158 0.434005 0.6186115 0.1109533 0.6179833 0.05785548 0.6313902 0.05672484 0.6319586 0.1121207 0.6313902 0.05672484 0.6463179 0.05206131 0.5647277 0.0578795 0.563474 0.04464906 0.6189266 0.04444664 0.563474 0.04464906 0.5587999 0.03054726 0.6233487 0.02949005 0.5647277 0.0578795 0.5653417 0.1110531 0.5520683 0.1120904 0.5514918 0.05681538 0.5520683 0.1120904 0.537395 0.1165966 0.5653417 0.1110531 0.6186115 0.1109533 0.6196037 0.1242463 0.564132 0.1243813 0.6196037 0.1242463 0.6240896 0.1390262 0.920629 0.6707581 0.9218659 0.2265568 0.9347499 0.2272918 0.9347499 0.2272918 0.9476367 0.2266263 0.9463997 0.670827 0.5670242 0.965082 0.5541267 0.9657429 0.555373 0.5215635 0.555373 0.5215635 0.5541267 0.9657429 0.5412353 0.9650067 0.6337562 0.9649829 0.6349689 0.520831 0.6478716 0.5215623 0.6478716 0.5215623 0.6607772 0.5208992 0.6595652 0.9650505 0.8538994 0.670836 0.8410139 0.6714993 0.8422524 0.2272896 0.8422524 0.2272896 0.8410139 0.6714993 0.8281325 0.6707614 0.7597498 0.2586472 0.7703812 0.2586208 0.7703812 0.4320634 0.8293721 0.2265521 0.8281325 0.6707614 0.8191422 0.6698262 0.6607772 0.5208992 0.6685189 0.5201979 0.6685189 0.9641739 0.6233487 0.02949005 0.5587999 0.03054726 0.5587301 0.02542001 0.7335389 0.2620936 0.7236471 0.2729968 0.5865367 0.2754362 0.5704158 0.434005 0.5787408 0.4229991 0.7223314 0.4201838 0.7314437 0.4281437 0.7224587 0.4200533 0.7237446 0.2732378 0.5742035 0.2677037 0.5864794 0.2756844 0.5785265 0.4229285 0.7364018 0.2361788 0.5702925 0.2408316 0.5700052 0.2305752 0.5682703 0.5209023 0.5760124 0.5201979 0.5760124 0.9641988 0.6463179 0.05206131 0.6514748 0.05206131 0.6514748 0.1167042 0.6349689 0.520831 0.6337562 0.9649829 0.6247736 0.9640501 0.9218659 0.2265568 0.920629 0.6707581 0.9116415 0.6698235 0.7362783 0.4560214 0.7365928 0.4664415 0.5669806 0.4714367 0.8551378 0.2266262 0.8628804 0.2259224 0.8628804 0.6699542 0.537357 0.05238252 0.537395 0.1165966 0.5322986 0.1165956 0.9476367 0.2266263 0.9553791 0.2259224 0.9553791 0.6699452 0.5471693 0.2637142 0.5429396 0.4374279 0.5322717 0.4371595 0.5424789 0.5208275 0.5412353 0.9650067 0.5322717 0.9640774 0.6240896 0.1390262 0.6240625 0.1441362 0.5595021 0.1437476 0.02438068 0.4274392 0.4835041 0.4737922 0.4835041 0.533547 0.9756194 0.8766642 0.8191422 0.8764122 0.8192679 0.7205649 0.4835104 0.6288304 0.4835104 0.7809193 0.02438056 0.7345703 0.02438056 0.378678 0.02438068 0.2259224 0.4835105 0.2724065 0.02438056 0.1771612 0.02438056 0.02438056 0.4835045 0.07090485 + + + + + + + + + + + + + + +

21 0 0 4 1 1 10 2 2 2 3 3 9 4 4 3 5 5 0 6 6 7 7 7 1 8 8 3 5 9 10 2 10 4 1 11 1 8 12 8 9 13 2 3 14 0 6 15 5 10 16 6 11 17 10 2 18 14 12 19 15 13 20 7 7 21 13 14 22 8 9 23 6 11 24 5 10 25 11 15 26 21 0 27 10 2 28 15 13 29 8 9 30 14 12 31 9 4 32 6 11 33 12 16 34 7 7 35 14 12 36 20 17 37 15 13 38 12 16 39 18 18 40 13 14 41 11 15 42 5 10 43 16 19 44 21 0 45 15 13 46 20 17 47 13 14 48 19 20 49 14 12 50 12 16 51 16 19 52 17 21 53 20 17 54 25 22 55 26 23 56 17 21 57 24 24 58 18 18 59 16 19 60 5 10 61 22 25 62 21 0 63 20 17 64 26 23 65 19 20 66 24 24 67 25 22 68 16 19 69 23 26 70 17 21 71 25 22 72 31 27 73 26 23 74 23 26 75 29 28 76 24 24 77 22 25 78 5 10 79 27 29 80 21 0 81 26 23 82 31 27 83 25 22 84 29 28 85 30 30 86 23 26 87 27 29 88 28 31 89 28 31 90 2 3 91 29 28 92 27 29 93 5 10 94 0 6 95 21 0 96 31 27 97 4 1 98 29 28 99 3 5 100 30 30 101 27 29 102 1 8 103 28 31 104 31 27 105 3 5 106 4 1 107 73 32 108 60 33 109 55 34 110 32 35 111 35 36 112 37 37 113 33 38 114 35 36 115 34 39 116 98 40 117 35 36 118 36 41 119 82 42 120 40 43 121 42 44 122 80 45 123 40 43 124 39 46 125 38 47 126 40 43 127 41 48 128 43 49 129 45 50 130 47 51 131 93 52 132 45 50 133 44 53 134 81 54 135 45 50 136 46 55 137 48 56 138 52 57 139 54 58 140 49 59 141 52 57 142 51 60 143 50 61 144 52 57 145 53 62 146 83 63 147 57 64 148 59 65 149 99 66 150 57 64 151 56 67 152 55 34 153 57 64 154 58 68 155 84 69 156 62 70 157 64 71 158 60 33 159 62 70 160 61 72 161 79 73 162 62 70 163 63 74 164 65 75 165 69 76 166 71 77 167 66 78 168 69 76 169 68 79 170 67 80 171 69 76 172 70 81 173 72 82 174 76 83 175 78 84 176 73 32 177 76 83 178 75 85 179 74 86 180 76 83 181 77 87 182 82 42 183 34 39 184 32 35 185 42 44 186 33 38 187 34 39 188 80 45 189 47 51 190 41 48 191 41 48 192 81 54 193 38 47 194 48 56 195 46 55 196 93 52 197 54 58 198 81 54 199 46 55 200 98 40 201 53 62 202 49 59 203 36 41 204 50 61 205 53 62 206 55 34 207 61 72 208 59 65 209 59 65 210 84 69 211 83 63 212 66 78 213 63 74 214 60 33 215 68 79 216 79 73 217 63 74 218 66 78 219 75 85 220 70 81 221 70 81 222 72 82 223 67 80 224 73 32 225 58 68 226 77 87 227 77 87 228 99 66 229 74 86 230 48 56 231 78 84 232 51 60 233 78 84 234 49 59 235 51 60 236 32 35 237 56 67 238 83 63 239 56 67 240 98 40 241 99 66 242 82 42 243 64 71 244 39 46 245 64 71 246 80 45 247 39 46 248 93 52 249 71 77 250 67 80 251 71 77 252 43 49 253 65 75 254 80 0 255 90 0 256 43 0 257 65 88 258 90 88 259 91 88 260 79 89 261 89 89 262 80 89 263 79 90 264 91 90 265 92 90 266 38 91 267 105 91 268 33 91 269 50 92 270 106 92 271 81 92 272 81 93 273 104 93 274 38 93 275 33 94 276 107 94 277 50 94 278 82 0 279 85 0 280 86 0 281 83 95 282 85 95 283 32 95 284 84 10 285 88 10 286 83 10 287 84 96 288 86 96 289 87 96 290 72 97 291 95 97 292 96 97 293 93 0 294 95 0 295 48 0 296 67 98 297 94 98 298 93 98 299 67 10 300 96 10 301 97 10 302 74 99 303 100 99 304 49 99 305 98 100 306 100 100 307 101 100 308 99 101 309 101 101 310 102 101 311 99 102 312 103 102 313 74 102 314 112 103 315 114 103 316 113 103 317 127 104 318 124 104 319 126 104 320 109 105 321 111 105 322 110 105 323 117 106 324 119 106 325 118 106 326 121 107 327 123 107 328 122 107 329 2 3 330 8 9 331 9 4 332 0 6 333 6 11 334 7 7 335 3 5 336 9 4 337 10 2 338 1 8 339 7 7 340 8 9 341 10 2 342 9 4 343 14 12 344 7 7 345 12 16 346 13 14 347 8 9 348 13 14 349 14 12 350 6 11 351 11 15 352 12 16 353 14 12 354 19 20 355 20 17 356 12 16 357 17 21 358 18 18 359 13 14 360 18 18 361 19 20 362 12 16 363 11 15 364 16 19 365 20 17 366 19 20 367 25 22 368 17 21 369 23 26 370 24 24 371 19 20 372 18 18 373 24 24 374 16 19 375 22 25 376 23 26 377 25 22 378 30 30 379 31 27 380 23 26 381 28 31 382 29 28 383 25 22 384 24 24 385 29 28 386 23 26 387 22 25 388 27 29 389 28 31 390 1 8 391 2 3 392 29 28 393 2 3 394 3 5 395 27 29 396 0 6 397 1 8 398 31 27 399 30 30 400 3 5 401 73 32 402 66 78 403 60 33 404 32 35 405 34 39 406 35 36 407 33 38 408 36 41 409 35 36 410 98 40 411 37 37 412 35 36 413 82 42 414 39 46 415 40 43 416 80 45 417 41 48 418 40 43 419 38 47 420 42 44 421 40 43 422 43 49 423 44 53 424 45 50 425 93 52 426 46 55 427 45 50 428 81 54 429 47 51 430 45 50 431 48 56 432 51 60 433 52 57 434 49 59 435 53 62 436 52 57 437 50 61 438 54 58 439 52 57 440 83 63 441 56 67 442 57 64 443 99 66 444 58 68 445 57 64 446 55 34 447 59 65 448 57 64 449 84 69 450 61 72 451 62 70 452 60 33 453 63 74 454 62 70 455 79 73 456 64 71 457 62 70 458 65 75 459 68 79 460 69 76 461 66 78 462 70 81 463 69 76 464 67 80 465 71 77 466 69 76 467 72 82 468 75 85 469 76 83 470 73 32 471 77 87 472 76 83 473 74 86 474 78 84 475 76 83 476 82 42 477 42 44 478 34 39 479 42 44 480 38 47 481 33 38 482 80 45 483 43 49 484 47 51 485 41 48 486 47 51 487 81 54 488 48 56 489 54 58 490 46 55 491 54 58 492 50 61 493 81 54 494 98 40 495 36 41 496 53 62 497 36 41 498 33 38 499 50 61 500 55 34 501 60 33 502 61 72 503 59 65 504 61 72 505 84 69 506 66 78 507 68 79 508 63 74 509 68 79 510 65 75 511 79 73 512 66 78 513 73 32 514 75 85 515 70 81 516 75 85 517 72 82 518 73 32 519 55 34 520 58 68 521 77 87 522 58 68 523 99 66 524 48 56 525 72 82 526 78 84 527 78 84 528 74 86 529 49 59 530 32 35 531 37 37 532 56 67 533 56 67 534 37 37 535 98 40 536 82 42 537 84 69 538 64 71 539 64 71 540 79 73 541 80 45 542 93 52 543 44 53 544 71 77 545 71 77 546 44 53 547 43 49 548 80 108 549 89 108 550 90 108 551 65 109 552 43 109 553 90 109 554 79 110 555 92 110 556 89 110 557 79 111 558 65 111 559 91 111 560 38 91 561 104 91 562 105 91 563 50 112 564 107 112 565 106 112 566 81 113 567 106 113 568 104 113 569 33 114 570 105 114 571 107 114 572 82 0 573 32 0 574 85 0 575 83 115 576 88 115 577 85 115 578 84 10 579 87 10 580 88 10 581 84 116 582 82 116 583 86 116 584 72 117 585 48 117 586 95 117 587 93 0 588 94 0 589 95 0 590 67 118 591 97 118 592 94 118 593 67 10 594 72 10 595 96 10 596 74 119 597 103 119 598 100 119 599 98 0 600 49 0 601 100 0 602 99 120 603 98 120 604 101 120 605 99 121 606 102 121 607 103 121 608 112 122 609 115 122 610 114 122 611 127 10 612 125 10 613 124 10 614 109 123 615 108 123 616 111 123 617 117 124 618 116 124 619 119 124 620 121 125 621 120 125 622 123 125 623

+
+
+
+
+ + + + 2 0 0 0 0 2 0 0 0 0 2 0 0 0 0 1 + + ping-marker-ball ping-marker-body base + + + + + + + + 1 0 0 2.51159e-5 0 0 1 -1.13652 0 -1 0 -1.02893e-5 0 0 0 1 1 0 0 -1.92262e-5 0 0 1 -5.238245 0 -1 0 7.9833e-6 0 0 0 1 1 0 0 0 0 0 1 0 0 -1 0 0 0 0 0 1 + + + + + + + + 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 + + + + + + + + + + + + + + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 0 0 2 1 0 2 2 3 0 4 2 5 0 6 2 7 0 8 2 9 0 10 2 11 0 12 2 13 0 14 2 15 0 16 2 17 0 18 2 19 0 20 2 21 0 22 2 23 0 24 2 25 0 26 2 27 0 28 2 29 0 30 2 31 0 32 2 33 0 34 2 35 0 36 2 37 0 38 2 39 0 40 2 41 0 42 2 43 0 44 2 45 0 46 2 47 0 48 2 49 0 50 2 51 0 52 2 53 0 54 2 55 0 56 2 57 0 58 2 59 0 60 2 61 0 62 2 63 1 64 2 65 1 66 2 67 1 68 2 69 1 70 2 71 1 72 2 73 1 74 2 75 1 76 2 77 1 78 2 79 1 80 2 81 1 82 2 83 1 84 2 85 1 86 2 87 1 88 2 89 1 90 2 91 1 92 2 93 1 94 2 95 1 96 2 97 1 98 2 99 1 100 2 101 1 102 2 103 1 104 2 105 1 106 2 107 1 108 2 109 1 110 2 111 1 112 2 113 1 114 2 115 1 116 2 117 1 118 2 119 1 120 2 121 1 122 2 123 1 124 2 125 1 126 2 127 1 128 2 129 1 130 2 131 1 132 2 133 1 134 2 135 1 136 2 137 1 138 2 139 1 140 2 141 1 142 2 143 1 144 2 145 1 146 2 147 1 148 2 149 1 150 2 151 1 152 2 153 1 154 2 155 1 156 2 157 1 158 2 159 1 160 2 161 1 162 2 163 1 164 2 165 1 166 2 167 1 168 2 169 1 170 2 171 1 172 2 173 1 174 2 175 1 176 2 177 1 178 2 179 1 180 2 181 1 182 2 183 1 184 2 185 1 186 2 187 1 188 2 189 1 190 2 191 1 192 2 193 1 194 2 195 1 196 2 197 1 198 2 199 1 200 2 201 1 202 2 203 1 204 2 205 1 206 2 207 1 208 2 209 1 210 2 211 1 212 2 213 1 214 2 215 1 216 2 217 1 218 2 219 1 220 2 221 1 222 2 223 1 224 2 225 1 226 2 227 1 228 2 229 1 230 2 231 1 232 2 233 1 234 2 235 1 236 2 237 1 238 2 239 1 240 2 241 1 242 2 243 1 244 2 245 1 246 2 247 1 248 2 249 1 250 2 251 1 252 2 253 1 254 2 255 + + + + + + + + + 0 0.04166662 0.08333331 0.125 0.1666666 0.2083333 0.25 0.2916666 0.3333333 0.375 0.4166666 0.4583333 0.5 0.5416667 0.5833333 0.625 0.6666667 0.7083333 0.75 0.7916667 0.8333333 0.875 0.9166667 0.9583333 1 1.041667 1.083333 1.125 1.166667 1.208333 1.25 1.291667 1.333333 1.375 1.416667 1.458333 1.5 1.541667 1.583333 1.625 1.666667 1.708333 1.75 1.791667 1.833333 1.875 1.916667 1.958333 2 2.041667 2.083333 2.125 2.166667 2.208333 2.25 2.291667 2.333333 2.375 2.416667 2.458333 2.5 2.541667 2.583333 2.625 2.666667 2.708333 2.75 2.791667 2.833333 2.875 2.916667 2.958333 3 3.041667 3.083333 3.125 3.166667 3.208333 3.25 3.291667 3.333333 3.375 3.416667 3.458333 3.5 3.541667 3.583333 3.625 3.666667 3.708333 3.75 3.791667 3.833333 3.875 3.916667 3.958333 4 4.041666 4.083333 4.125 4.166666 + + + + + + + + 1 0 0 0 0 0 -1 0 0 1 0 0 0 0 0 1 0.9983632 0 0.05719231 0 0.05719231 0 -0.9983632 0 0 1 0 0 0 0 0 1 0.9933116 0 0.1154642 0 0.1154642 0 -0.9933116 0 0 1 0 0 0 0 0 1 0.9846505 0 0.1745377 0 0.1745377 0 -0.9846505 0 0 1 0 0 0 0 0 1 0.9722123 0 0.234101 0 0.234101 0 -0.9722123 0 0 1 0 0 0 0 0 1 0.9558632 0 0.293812 0 0.293812 0 -0.9558632 0 0 1 0 0 0 0 0 1 0.9355093 0 0.353302 0 0.353302 0 -0.9355093 0 0 1 0 0 0 0 0 1 0.9111016 0 0.4121819 0 0.4121819 0 -0.9111016 0 0 1 0 0 0 0 0 1 0.8826405 0 0.4700486 0 0.4700486 0 -0.8826405 0 0 1 0 0 0 0 0 1 0.8501794 0 0.5264931 0 0.5264931 0 -0.8501794 0 0 1 0 0 0 0 0 1 0.813826 0 0.5811087 0 0.5811087 0 -0.813826 0 0 1 0 0 0 0 0 1 0.7737424 0 0.6335003 0 0.6335003 0 -0.7737424 0 0 1 0 0 0 0 0 1 0.7301437 0 0.6832936 0 0.6832936 0 -0.7301437 0 0 1 0 0 0 0 0 1 0.6832936 0 0.7301437 0 0.7301437 0 -0.6832936 0 0 1 0 0 0 0 0 1 0.6335002 0 0.7737426 0 0.7737426 0 -0.6335002 0 0 1 0 0 0 0 0 1 0.5811086 0 0.813826 0 0.813826 0 -0.5811086 0 0 1 0 0 0 0 0 1 0.526493 0 0.8501795 0 0.8501795 0 -0.526493 0 0 1 0 0 0 0 0 1 0.4700487 0 0.8826405 0 0.8826405 0 -0.4700487 0 0 1 0 0 0 0 0 1 0.412182 0 0.9111015 0 0.9111015 0 -0.412182 0 0 1 0 0 0 0 0 1 0.353302 0 0.9355093 0 0.9355093 0 -0.353302 0 0 1 0 0 0 0 0 1 0.2938119 0 0.9558633 0 0.9558633 0 -0.2938119 0 0 1 0 0 0 0 0 1 0.2341012 0 0.9722121 0 0.9722121 0 -0.2341012 0 0 1 0 0 0 0 0 1 0.1745378 0 0.9846504 0 0.9846504 0 -0.1745378 0 0 1 0 0 0 0 0 1 0.1154642 0 0.9933116 0 0.9933116 0 -0.1154642 0 0 1 0 0 0 0 0 1 0.05719256 0 0.998363 0 0.998363 0 -0.05719256 0 0 1 0 0 0 0 0 1 -1.34359e-7 0 1 0 1 0 1.34359e-7 0 0 1 0 0 0 0 0 1 -0.05719216 0 0.9983631 0 0.9983631 0 0.05719216 0 0 1 0 0 0 0 0 1 -0.1154643 0 0.9933117 0 0.9933117 0 0.1154643 0 0 1 0 0 0 0 0 1 -0.1745376 0 0.9846504 0 0.9846504 0 0.1745376 0 0 1 0 0 0 0 0 1 -0.2341008 0 0.9722121 0 0.9722121 0 0.2341008 0 0 1 0 0 0 0 0 1 -0.293812 0 0.9558632 0 0.9558632 0 0.293812 0 0 1 0 0 0 0 0 1 -0.3533019 0 0.9355093 0 0.9355093 0 0.3533019 0 0 1 0 0 0 0 0 1 -0.412182 0 0.9111015 0 0.9111015 0 0.412182 0 0 1 0 0 0 0 0 1 -0.4700488 0 0.8826405 0 0.8826405 0 0.4700488 0 0 1 0 0 0 0 0 1 -0.5264931 0 0.8501795 0 0.8501795 0 0.5264931 0 0 1 0 0 0 0 0 1 -0.5811087 0 0.8138261 0 0.8138261 0 0.5811087 0 0 1 0 0 0 0 0 1 -0.6335001 0 0.7737424 0 0.7737424 0 0.6335001 0 0 1 0 0 0 0 0 1 -0.6832936 0 0.7301437 0 0.7301437 0 0.6832936 0 0 1 0 0 0 0 0 1 -0.7301437 0 0.6832936 0 0.6832936 0 0.7301437 0 0 1 0 0 0 0 0 1 -0.7737427 0 0.6335003 0 0.6335003 0 0.7737427 0 0 1 0 0 0 0 0 1 -0.813826 0 0.5811086 0 0.5811086 0 0.813826 0 0 1 0 0 0 0 0 1 -0.8501792 0 0.5264929 0 0.5264929 0 0.8501792 0 0 1 0 0 0 0 0 1 -0.8826406 0 0.4700486 0 0.4700486 0 0.8826406 0 0 1 0 0 0 0 0 1 -0.9111014 0 0.4121819 0 0.4121819 0 0.9111014 0 0 1 0 0 0 0 0 1 -0.9355093 0 0.3533019 0 0.3533019 0 0.9355093 0 0 1 0 0 0 0 0 1 -0.9558632 0 0.2938119 0 0.2938119 0 0.9558632 0 0 1 0 0 0 0 0 1 -0.972212 0 0.234101 0 0.234101 0 0.972212 0 0 1 0 0 0 0 0 1 -0.9846506 0 0.1745377 0 0.1745377 0 0.9846506 0 0 1 0 0 0 0 0 1 -0.9933118 0 0.1154643 0 0.1154643 0 0.9933118 0 0 1 0 0 0 0 0 1 -0.9983631 0 0.05719225 0 0.05719225 0 0.9983631 0 0 1 0 0 0 0 0 1 -1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 -0.9983629 0 -0.05719229 0 -0.05719229 0 0.9983629 0 0 1 0 0 0 0 0 1 -0.9933118 0 -0.1154642 0 -0.1154642 0 0.9933118 0 0 1 0 0 0 0 0 1 -0.9846506 0 -0.1745377 0 -0.1745377 0 0.9846506 0 0 1 0 0 0 0 0 1 -0.9722125 0 -0.234101 0 -0.234101 0 0.9722125 0 0 1 0 0 0 0 0 1 -0.9558632 0 -0.2938119 0 -0.2938119 0 0.9558632 0 0 1 0 0 0 0 0 1 -0.9355093 0 -0.353302 0 -0.353302 0 0.9355093 0 0 1 0 0 0 0 0 1 -0.9111014 0 -0.4121819 0 -0.4121819 0 0.9111014 0 0 1 0 0 0 0 0 1 -0.8826404 0 -0.4700486 0 -0.4700486 0 0.8826404 0 0 1 0 0 0 0 0 1 -0.8501792 0 -0.526493 0 -0.526493 0 0.8501792 0 0 1 0 0 0 0 0 1 -0.813826 0 -0.5811085 0 -0.5811085 0 0.813826 0 0 1 0 0 0 0 0 1 -0.7737426 0 -0.6335003 0 -0.6335003 0 0.7737426 0 0 1 0 0 0 0 0 1 -0.7301437 0 -0.6832937 0 -0.6832937 0 0.7301437 0 0 1 0 0 0 0 0 1 -0.6832939 0 -0.7301438 0 -0.7301438 0 0.6832939 0 0 1 0 0 0 0 0 1 -0.6335003 0 -0.7737425 0 -0.7737425 0 0.6335003 0 0 1 0 0 0 0 0 1 -0.5811086 0 -0.813826 0 -0.813826 0 0.5811086 0 0 1 0 0 0 0 0 1 -0.5264929 0 -0.8501794 0 -0.8501794 0 0.5264929 0 0 1 0 0 0 0 0 1 -0.4700485 0 -0.8826405 0 -0.8826405 0 0.4700485 0 0 1 0 0 0 0 0 1 -0.4121818 0 -0.9111015 0 -0.9111015 0 0.4121818 0 0 1 0 0 0 0 0 1 -0.3533017 0 -0.9355092 0 -0.9355092 0 0.3533017 0 0 1 0 0 0 0 0 1 -0.2938122 0 -0.9558634 0 -0.9558634 0 0.2938122 0 0 1 0 0 0 0 0 1 -0.2341008 0 -0.9722122 0 -0.9722122 0 0.2341008 0 0 1 0 0 0 0 0 1 -0.1745378 0 -0.9846506 0 -0.9846506 0 0.1745378 0 0 1 0 0 0 0 0 1 -0.1154643 0 -0.9933116 0 -0.9933116 0 0.1154643 0 0 1 0 0 0 0 0 1 -0.05719216 0 -0.9983631 0 -0.9983631 0 0.05719216 0 0 1 0 0 0 0 0 1 -1.34359e-7 0 -1 0 -1 0 1.34359e-7 0 0 1 0 0 0 0 0 1 0.0570372 0 -0.9983721 0 -0.9983721 0 -0.0570372 0 0 1 0 0 0 0 0 1 0.1151472 0 -0.9933485 0 -0.9933485 0 -0.1151472 0 0 1 0 0 0 0 0 1 0.1740543 0 -0.9847361 0 -0.9847361 0 -0.1740543 0 0 1 0 0 0 0 0 1 0.2334493 0 -0.9723691 0 -0.9723691 0 -0.2334493 0 0 1 0 0 0 0 0 1 0.2929935 0 -0.9561144 0 -0.9561144 0 -0.2929935 0 0 1 0 0 0 0 0 1 0.3523213 0 -0.9358791 0 -0.9358791 0 -0.3523213 0 0 1 0 0 0 0 0 1 0.4110473 0 -0.911614 0 -0.911614 0 -0.4110473 0 0 1 0 0 0 0 0 1 0.4687718 0 -0.8833194 0 -0.8833194 0 -0.4687718 0 0 1 0 0 0 0 0 1 0.5250899 0 -0.8510468 0 -0.8510468 0 -0.5250899 0 0 1 0 0 0 0 0 1 0.579598 0 -0.8149026 0 -0.8149026 0 -0.579598 0 0 1 0 0 0 0 0 1 0.6319049 0 -0.7750459 0 -0.7750459 0 -0.6319049 0 0 1 0 0 0 0 0 1 0.6816387 0 -0.731689 0 -0.731689 0 -0.6816387 0 0 1 0 0 0 0 0 1 0.7284566 0 -0.685092 0 -0.685092 0 -0.7284566 0 0 1 0 0 0 0 0 1 0.7720523 0 -0.635559 0 -0.635559 0 -0.7720523 0 0 1 0 0 0 0 0 1 0.8121629 0 -0.5834306 0 -0.5834306 0 -0.8121629 0 0 1 0 0 0 0 0 1 0.8485736 0 -0.5290774 0 -0.5290774 0 -0.8485736 0 0 1 0 0 0 0 0 1 0.8811216 0 -0.4728898 0 -0.4728898 0 -0.8811216 0 0 1 0 0 0 0 0 1 0.9096981 0 -0.4152703 0 -0.4152703 0 -0.9096981 0 0 1 0 0 0 0 0 1 0.9342478 0 -0.3566245 0 -0.3566245 0 -0.9342478 0 0 1 0 0 0 0 0 1 0.9547679 0 -0.2973522 0 -0.2973522 0 -0.9547679 0 0 1 0 0 0 0 0 1 0.9713044 0 -0.2378396 0 -0.2378396 0 -0.9713044 0 0 1 0 0 0 0 0 1 0.9839484 0 -0.1784533 0 -0.1784533 0 -0.9839484 0 0 1 0 0 0 0 0 1 0.9928301 0 -0.119534 0 -0.119534 0 -0.9928301 0 0 1 0 0 0 0 0 1 0.9981138 0 -0.06139208 0 -0.06139208 0 -0.9981138 0 0 1 0 0 0 0 0 1 0.9999908 0 -0.004305305 0 -0.004305305 0 -0.9999908 0 0 1 0 0 0 0 0 1 + + + + + + + + LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR + + + + + + + + + + + + + + + + 0 0.04166662 0.08333331 0.125 0.1666666 0.2083333 0.25 0.2916666 0.3333333 0.375 0.4166666 0.4583333 0.5 0.5416667 0.5833333 0.625 0.6666667 0.7083333 0.75 0.7916667 0.8333333 0.875 0.9166667 0.9583333 1 1.041667 1.083333 1.125 1.166667 1.208333 1.25 1.291667 1.333333 1.375 1.416667 1.458333 1.5 1.541667 1.583333 1.625 1.666667 1.708333 1.75 1.791667 1.833333 1.875 1.916667 1.958333 2 2.041667 2.083333 2.125 2.166667 2.208333 2.25 2.291667 2.333333 2.375 2.416667 2.458333 2.5 2.541667 2.583333 2.625 2.666667 2.708333 2.75 2.791667 2.833333 2.875 2.916667 2.958333 3 3.041667 3.083333 3.125 3.166667 3.208333 3.25 3.291667 3.333333 3.375 3.416667 3.458333 3.5 3.541667 3.583333 3.625 3.666667 3.708333 3.75 3.791667 3.833333 3.875 3.916667 3.958333 4 4.041666 4.083333 4.125 4.166666 + + + + + + + + 1 0 0 -2.51159e-5 0 1 0 1.13652 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.151992 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.167464 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.182937 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.198409 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.213881 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.229353 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.244825 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.260297 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.275769 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.291241 0 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.306713 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.322185 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.337657 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 2.98023e-8 -2.51159e-5 0 1 0 1.353129 -2.98023e-8 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.368601 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.384073 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.399545 0 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.415017 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.430489 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.445961 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.461433 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.476906 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.492378 0 0 1 1.02893e-5 0 0 0 1 1 0 -3.72529e-9 -2.51159e-5 0 1 0 1.50785 3.72529e-9 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.523322 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.50785 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.492378 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.476905 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.461433 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.445961 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.430489 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.415017 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.399545 0 0 0.9999999 1.02892e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.384073 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.368601 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.353129 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.337657 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.322185 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.306713 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.291241 0 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.275769 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.260297 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.244825 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.229353 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.213881 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.198409 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.182937 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.167464 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.151992 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.13652 0 0 1 1.02893e-5 0 0 0 1 1 0 3.72529e-9 -2.51159e-5 0 1 0 1.151992 -3.72529e-9 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.167464 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.182937 0 0 1 1.02893e-5 0 0 0 1 1 0 1.49012e-8 -2.51159e-5 0 1 0 1.198409 -1.49012e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.213881 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.229353 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.244825 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.260297 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.275769 0 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.291241 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.306713 2.98023e-8 0 1 1.02892e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.322185 0 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.337657 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.353129 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.368601 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.384073 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.399545 0 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.415017 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.430489 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 0.9999999 0 -2.98023e-8 -2.51159e-5 0 1 0 1.445961 2.98023e-8 0 0.9999999 1.02892e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.461433 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.476906 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.492378 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.50785 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.523322 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.508202 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.493082 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.477962 0 0 0.9999999 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.462842 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.447722 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.432603 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.417483 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.402363 0 0 0.9999999 1.02892e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.387243 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 -2.98023e-8 -2.51159e-5 0 1 0 1.372123 2.98023e-8 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.357003 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.341883 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.326764 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.311644 0 0 1 1.02892e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.296524 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.281404 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.266284 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.251164 0 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.236044 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.220924 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.205805 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.190685 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.175565 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.160445 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.145325 0 0 1 1.02893e-5 0 0 0 1 + + + + + + + + LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR + + + + + + + + + + + + + + + + 0 0.04166662 0.08333331 0.125 0.1666666 0.2083333 0.25 0.2916666 0.3333333 0.375 0.4166666 0.4583333 0.5 0.5416667 0.5833333 0.625 0.6666667 0.7083333 0.75 0.7916667 0.8333333 0.875 0.9166667 0.9583333 1 1.041667 1.083333 1.125 1.166667 1.208333 1.25 1.291667 1.333333 1.375 1.416667 1.458333 1.5 1.541667 1.583333 1.625 1.666667 1.708333 1.75 1.791667 1.833333 1.875 1.916667 1.958333 2 2.041667 2.083333 2.125 2.166667 2.208333 2.25 2.291667 2.333333 2.375 2.416667 2.458333 2.5 2.541667 2.583333 2.625 2.666667 2.708333 2.75 2.791667 2.833333 2.875 2.916667 2.958333 3 3.041667 3.083333 3.125 3.166667 3.208333 3.25 3.291667 3.333333 3.375 3.416667 3.458333 3.5 3.541667 3.583333 3.625 3.666667 3.708333 3.75 3.791667 3.833333 3.875 3.916667 3.958333 4 4.041666 4.083333 4.125 4.166666 + + + + + + + + 1 0 0 1.92262e-5 0 1 0 5.238245 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.253717 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.269189 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.284661 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.300133 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.315605 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.331077 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.34655 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.362021 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.377493 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.392965 0 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.408438 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.42391 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.439382 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 2.98023e-8 1.92262e-5 0 1 0 5.454854 -2.98023e-8 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.470326 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.485798 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.50127 0 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.516742 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.532214 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.547686 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.563158 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.57863 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.594102 0 0 1 -7.9833e-6 0 0 0 1 1 0 -3.72529e-9 1.92262e-5 0 1 0 5.609574 3.72529e-9 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.625046 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.609574 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.594102 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.57863 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.563158 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.547686 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.532214 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.516742 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.50127 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.485798 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.470326 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.454854 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.439382 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.42391 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.408438 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.392965 0 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.377493 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.362021 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.34655 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.331077 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.315605 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.300133 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.284661 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.269189 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.253717 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.238245 0 0 1 -7.9833e-6 0 0 0 1 1 0 3.72529e-9 1.92262e-5 0 1 0 5.253717 -3.72529e-9 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.269189 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.284661 0 0 1 -7.9833e-6 0 0 0 1 1 0 1.49012e-8 1.92262e-5 0 1 0 5.300133 -1.49012e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.315605 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.331077 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.34655 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.362021 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.377493 0 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.392965 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.408438 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.42391 0 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.439382 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.454854 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.470326 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.485798 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.50127 0 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.516742 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.532214 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 -2.98023e-8 1.92262e-5 0 1 0 5.547686 2.98023e-8 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.563158 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.57863 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.594102 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.609574 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.625046 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.609646 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.594247 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.578847 0 0 0.9999999 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.563447 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.548048 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.532648 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.517248 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.501848 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.486448 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 -2.98023e-8 1.92262e-5 0 1 0 5.471049 2.98023e-8 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.455649 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.440249 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.42485 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.40945 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.39405 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.37865 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.36325 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.34785 0 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.332451 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.317051 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.301651 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.286252 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.270852 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.255452 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.240052 0 0 1 -7.9833e-6 0 0 0 1 + + + + + + + + LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR + + + + + + + + + + + + + + + + + + + 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 + + 1 0 0 0 0 0 -1 0 0 1 0 0 0 0 0 1 + + 1 0 0 -2.51159e-5 0 1 0 1.13652 0 0 1 1.02893e-5 0 0 0 1 + + + 0 + 0 + 0 + 6 + + + + + 1 0 0 1.92262e-5 0 1 0 5.238245 0 0 1 -7.9833e-6 0 0 0 1 + + + 0 + 0 + 0 + 6 + + + + + 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 + + + 0 + 0 + 0 + 6 + + + + + + 0 + 0 + 0 + 6 + + + + + 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 + + #ping-marker-armature_base + + + + + + + + + + + + + + + + + + +
Index: ps/trunk/binaries/data/mods/public/art/meshes/skeletal/flare_target_marker.dae =================================================================== --- ps/trunk/binaries/data/mods/public/art/meshes/skeletal/flare_target_marker.dae (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/meshes/skeletal/flare_target_marker.dae (revision 25691) @@ -0,0 +1,313 @@ + + + + + Blender User + Blender 2.90.1 commit date:2020-09-23, commit time:06:43, hash:3e85bb34d0d7 + + 2021-04-22T18:38:35 + 2021-04-22T18:38:35 + + Z_UP + + + + + + + + 0 0 0 1 + + + 0.8 0.8 0.8 1 + + + 0.5 + + + + + + + + + + + 0 0 0 1 + + + 0.7679928 0 0.8000001 1 + + + 1.45 + + + + + + + + + + + + + + + + + + + + -1.2542e-5 0.16854 0.876138 -1.2542e-5 0.2919237 0.7460136 -1.2542e-5 0.3370852 0.5682601 -1.2542e-5 0.2919237 0.3905067 -1.2542e-5 0.16854 0.2603824 -1.25861e-5 -5.1786e-6 0.9237671 0.1459518 0.08426737 0.876138 0.2528052 0.1459592 0.7460136 0.2919163 0.16854 0.5682601 0.2528052 0.1459592 0.3905067 0.1459519 0.08426737 0.2603824 0.1459518 -0.08427774 0.876138 0.2528052 -0.1459696 0.7460136 0.2919163 -0.1685503 0.5682601 0.2528052 -0.1459696 0.3905067 0.1459518 -0.08427774 0.2603824 -1.2571e-5 -0.1685503 0.876138 -1.25861e-5 -0.291934 0.7460136 -1.25861e-5 -0.3370956 0.5682601 -1.2566e-5 -0.291934 0.3905067 -1.25761e-5 -0.1685503 0.2603824 -1.2542e-5 -5.15315e-6 0.2127533 -0.1459769 -0.08427768 0.876138 -0.2528303 -0.1459695 0.7460136 -0.2919414 -0.1685503 0.5682601 -0.2528303 -0.1459695 0.3905067 -0.145977 -0.08427768 0.2603824 -0.1459769 0.08426743 0.876138 -0.2528303 0.1459593 0.7460136 -0.2919414 0.1685401 0.5682601 -0.2528303 0.1459593 0.3905067 -0.1459769 0.08426749 0.2603824 -0.5031438 0.4067856 3.926643 -0.4191778 0.416755 4.019573 -0.4825602 0.4137057 3.992354 -0.4690158 0.4665592 3.980295 -0.4161745 0.4801436 3.992353 -0.4755474 0.4730942 3.926642 -0.4191778 -0.4167107 4.019573 -0.4755485 -0.4729414 3.926643 -0.4690163 -0.4664185 3.980296 -0.4161757 -0.4801049 3.992354 -0.4825598 -0.4135391 3.992355 0.4069955 -0.5029647 3.926642 0.473306 -0.4755504 3.926643 0.4667804 -0.4689928 3.980296 0.4803683 -0.4161868 3.992355 0.4139276 -0.4823904 3.992354 0.5016148 0.4075928 3.926643 0.4078046 0.5014013 3.926642 0.4176568 0.4174291 4.019573 0.4740707 0.4738581 3.926643 0.4675345 0.4673132 3.980296 0.4147034 0.4808134 3.992354 0.4810373 0.4144808 3.992355 -0.1518459 0.1500419 1.388505 -0.2271001 0.2252219 1.481435 -0.2096655 0.2078092 1.427782 -0.154374 0.218949 1.415724 -0.2207594 0.1525109 1.415723 -0.1518459 -0.1505176 1.388505 -0.220759 -0.1528535 1.415723 -0.209666 -0.208173 1.427782 -0.1543751 -0.2194195 1.415724 -0.2271012 -0.2255524 1.481435 0.1587484 -0.254209 1.481436 0.1498667 -0.1513479 1.388505 0.2527096 -0.1605827 1.481434 0.1523379 -0.2202646 1.415724 0.207639 -0.2093205 1.427782 0.2187784 -0.1540609 1.415723 0.2250588 -0.2267946 1.481435 0.2529672 0.15932 1.481434 0.1501096 0.1502852 1.388505 0.1591573 0.2531288 1.481436 0.2190256 0.1528641 1.415723 0.2079753 0.2081452 1.427782 0.152692 0.2191969 1.415724 0.2254233 0.2255855 1.481435 -0.1608152 -0.2533295 1.481436 -0.4092624 -0.5007183 3.926642 0.4169831 -0.4190118 4.019573 -0.5031437 -0.40658 3.926643 -0.2546963 0.1589133 1.481434 -0.2546963 -0.1591908 1.481434 -0.4529223 0.4067856 3.926643 -0.4529222 -0.40658 3.926643 -0.2293195 -0.1591908 1.481434 -0.2293195 0.1589133 1.481434 -0.4092624 -0.4506849 3.926642 0.4069955 -0.4527066 3.926642 0.1587484 -0.2288265 1.481436 -0.1608152 -0.228035 1.481436 0.5009569 -0.4093387 3.926643 0.4507682 -0.4093387 3.926643 0.4513604 0.4075928 3.926643 0.2275776 0.15932 1.481434 0.2273457 -0.1605827 1.481434 -0.4092603 0.500743 3.926642 -0.160813 0.252871 1.481436 0.4078046 0.4512226 3.926642 -0.4092603 0.4506303 3.926642 -0.160813 0.2275454 1.481436 0.1591573 0.2277774 1.481436 -0.4191778 -0.4167107 3.969573 -0.4191778 0.416755 3.969573 0.4169831 -0.4190118 3.969573 0.4176568 0.4174291 3.969573 -0.4529223 0.4067856 3.926643 -0.4529222 -0.40658 3.926643 -0.2293195 -0.1591908 1.481434 -0.2293195 0.1589133 1.481434 -0.4092624 -0.4506849 3.926642 0.4069955 -0.4527066 3.926642 0.1587484 -0.2288265 1.481436 -0.1608152 -0.228035 1.481436 0.4507682 -0.4093387 3.926643 0.4513604 0.4075928 3.926643 0.2275776 0.15932 1.481434 0.2273457 -0.1605827 1.481434 0.4078046 0.4512226 3.926642 -0.4092603 0.4506303 3.926642 -0.160813 0.2275454 1.481436 0.1591573 0.2277774 1.481436 -0.4191778 -0.4167107 3.969573 -0.4191778 0.416755 3.969573 0.4169831 -0.4190118 3.969573 0.4176568 0.4174291 3.969573 + + + + + + + + + + 0 0 -1 0 0.5755252 -0.8177841 0.4984077 0.2877648 -0.8177904 0 1 0 0.7727485 0.446131 -0.4514719 0 0.8922869 -0.451469 0 0.5755252 0.8177841 0.7727485 0.446131 0.4514719 0 0.8922869 0.451469 0.8660317 0.4999893 0 0 0 1 0.4984077 0.2877648 0.8177904 0.7727485 -0.446131 -0.4514719 0.4984077 -0.2877648 -0.8177904 0.8660317 -0.4999893 0 0.4984077 -0.2877648 0.8177904 0.7727485 -0.446131 0.4514719 0 -0.5755252 -0.8177841 0 -1 0 0 -0.5755252 0.8177841 0 -0.8922869 -0.451469 0 -0.8922869 0.451469 -0.7727485 -0.446131 -0.4514719 -0.4984077 -0.2877648 -0.8177904 -0.8660317 -0.4999893 0 -0.4984077 -0.2877648 0.8177904 -0.7727485 -0.446131 0.4514719 -0.4984077 0.2877648 -0.8177904 -0.8660317 0.4999893 0 -0.4984077 0.2877648 0.8177904 -0.7727485 0.446131 -0.4514719 -0.7727485 0.446131 0.4514719 0.169104 0.169104 -0.9709829 -0.1695967 -0.1691694 -0.9708856 -0.169319 0.1691969 -0.9709292 -0.9774976 -0.08664381 -0.1923316 -0.6105381 0.6100192 0.505094 -0.7073171 0.7067678 0.01348954 0.1089839 -0.1089228 0.988058 -0.7393861 0.1604999 0.6538715 0.08594143 0.9775537 -0.1923612 -0.1610834 0.7392194 0.6539167 -0.977478 0.08685845 -0.1923338 -0.6111707 -0.6094921 0.5049651 -0.7395038 -0.1603463 0.6537762 0.08432459 -0.9775921 -0.1928815 -0.7080187 -0.7060655 0.01345902 0.1093208 0.1090461 0.9880071 -0.1623929 -0.7390237 0.653814 -0.08896237 -0.9774574 -0.1914752 0.6091147 -0.6112816 0.5052863 0.1586377 -0.7396278 0.6540526 0.977559 0.08560621 -0.1924843 0.7057749 -0.7083079 0.01348924 -0.1084657 0.108832 0.9881249 0.7390979 -0.1612666 0.6540088 0.9774962 -0.08713203 -0.1921177 0.6102028 0.6102028 0.5052775 0.7393777 0.1600708 0.6539862 -0.08719253 0.977521 -0.191964 0.7070425 0.7070425 0.01348954 -0.1086786 -0.1086786 0.9881184 0.1600451 0.7393389 0.6540364 -0.9862205 -0.1552805 0.05707043 -0.5430032 0.5426064 -0.6408792 -0.6592218 0.136819 -0.7393966 0.1544885 0.9863219 0.05746775 -0.6803995 0.6798502 -0.2736064 -0.1373367 0.6591553 -0.73936 -0.9861934 0.1554641 0.05704009 -0.5436445 -0.5421796 -0.6406966 -0.6810982 -0.6792365 -0.2733915 -0.659361 -0.1367247 -0.7392899 0.1528075 -0.9865498 0.05804669 -0.1385578 -0.6590347 -0.7392395 -0.1576307 -0.9858863 0.0563991 0.5416525 -0.5436056 -0.6411751 0.6788417 -0.6812832 -0.2739111 0.1687701 -0.1693805 -0.9709928 0.1351404 -0.6594657 -0.7394878 0.9863629 0.1542718 0.05734455 0.6589313 -0.1374872 -0.7395315 0.9861475 -0.1558328 0.05682736 0.5426312 0.5426617 -0.6411471 0.680055 0.6800855 -0.2738776 0.6591807 0.1364201 -0.7395069 -0.1558014 0.9861419 0.05701017 0.1363905 0.6592156 -0.7394812 -0.9948861 0 0.1010045 0.9948778 0 0.1010854 3.16847e-7 0 1 1 0 0 -0.9999998 8.05407e-4 0 0.002752006 0.9999963 0 8.05503e-4 -0.9999997 0 0 -0.9949013 0.1008538 0 0.994921 0.1006592 0 -0.994885 0.1010152 0 0.9948652 0.1012097 -0.9948696 0 0.101166 -2.04709e-7 0 -1 0.9948779 0 0.1010853 -9.185e-7 0 1 -0.002466559 -0.9958544 -0.09092879 -7.9896e-7 0 1 -0.995845 0 -0.09106522 0.9958447 -7.2194e-4 -0.09106528 -7.22026e-4 0.9958571 -0.09092891 0 0 -1 -0.994886 0 0.1010048 0.9948779 0 0.101085 -9.18544e-7 0 1 -0.9999997 8.05336e-4 0 0.002751946 0.9999963 0 8.05492e-4 -0.9999997 0 0 -0.9949013 0.100854 0 0.994921 0.1006591 0 -0.994885 0.1010149 0 0.9948652 0.1012096 -0.9948696 0 0.101166 0.9948778 0 0.1010854 1.58028e-7 0 1 -0.002466559 -0.9958544 -0.09092879 -0.995845 0 -0.09106528 0.9958447 -7.21768e-4 -0.09106528 -7.22017e-4 0.9958571 -0.09092891 + + + + + + + + + + 0.2689294 0.9634857 0.2468196 0.9324201 0.2832639 0.9281525 0.2347516 0.8773799 0.2769502 0.895384 0.2382609 0.9014936 0.2276332 0.8173105 0.2720444 0.8479661 0.2324163 0.8524765 0.2382609 0.9014936 0.2832639 0.9281525 0.2468196 0.9324201 0.2324163 0.8524765 0.2744972 0.871675 0.2347516 0.8773799 0.2276332 0.8173105 0.244757 0.7815487 0.2657305 0.8151974 0.2832639 0.9281525 0.316578 0.8908735 0.3213611 0.9260395 0.2720444 0.8479661 0.3142428 0.8659701 0.2744972 0.871675 0.2657305 0.8151974 0.2800651 0.7798643 0.3021747 0.81093 0.3042375 0.9618013 0.2832639 0.9281525 0.3213611 0.9260395 0.2744972 0.871675 0.316578 0.8908735 0.2769502 0.895384 0.2657305 0.8151974 0.3107335 0.8418564 0.2720444 0.8479661 0.316578 0.8908735 0.3617075 0.9264094 0.3213611 0.9260395 0.3107335 0.8418564 0.3581165 0.8580409 0.3142428 0.8659701 0.3021747 0.81093 0.3117759 0.774842 0.337064 0.8023198 0.3411974 0.9629787 0.3213611 0.9260395 0.3617075 0.9264094 0.3142428 0.8659701 0.3607792 0.8871456 0.316578 0.8908735 0.3107335 0.8418564 0.337064 0.8023198 0.3511369 0.8312228 0.3617075 0.9264094 0.4242356 0.8883687 0.4047687 0.9338378 0.3511369 0.8312228 0.4212717 0.8423786 0.3581165 0.8580409 0.337064 0.8023198 0.334192 0.764118 0.3679613 0.7822087 0.3764712 0.9693508 0.3617075 0.9264094 0.4047687 0.9338378 0.3607792 0.8871456 0.4212717 0.8423786 0.4242356 0.8883687 0.337064 0.8023198 0.3993613 0.8057134 0.3511369 0.8312228 0.1496332 0.9376367 0.2119304 0.9410302 0.1810331 0.9611415 0.1247587 0.8549814 0.1908779 0.8853091 0.1277227 0.9009717 0.1442257 0.8095122 0.1725232 0.7739992 0.1872869 0.8169406 0.2148024 0.979232 0.1810331 0.9611415 0.2119304 0.9410302 0.1496332 0.9376367 0.1908779 0.8853091 0.1978574 0.9121273 0.1247587 0.8549814 0.1872869 0.8169406 0.1882152 0.8562045 0.1882152 0.8562045 0.2347516 0.8773799 0.1908779 0.8853091 0.1872869 0.8169406 0.2077969 0.7803713 0.2276332 0.8173105 0.2372184 0.9685081 0.2119304 0.9410302 0.2468196 0.9324201 0.1908779 0.8853091 0.2382609 0.9014936 0.1978574 0.9121273 0.1872869 0.8169406 0.2324163 0.8524765 0.1882152 0.8562045 0.2119304 0.9410302 0.2382609 0.9014936 0.2468196 0.9324201 0.5653417 0.1110531 0.6179833 0.05785548 0.6186115 0.1109533 0.5702925 0.2408316 0.5639945 0.2571572 0.5562849 0.2494693 0.5742035 0.2677037 0.5639945 0.2571572 0.5734679 0.2546974 0.5471693 0.2637142 0.5639945 0.2571572 0.5618067 0.2668063 0.7364018 0.2361788 0.7431017 0.2523872 0.7339112 0.2504816 0.7597498 0.2586472 0.7431017 0.2523872 0.7506223 0.2446186 0.7335389 0.2620936 0.7431017 0.2523872 0.7452447 0.2615504 0.759702 0.4320898 0.7427436 0.4390336 0.7451931 0.428944 0.7362783 0.4560214 0.7427436 0.4390336 0.7507525 0.446845 0.7314437 0.4281437 0.7427436 0.4390336 0.732751 0.4417741 0.5666657 0.4610032 0.5597555 0.4440929 0.5695233 0.446253 0.5429396 0.4374279 0.5597555 0.4440929 0.5519188 0.4520102 0.5704158 0.434005 0.5597555 0.4440929 0.5573531 0.4344296 0.646318 0.1167042 0.6300215 0.1224703 0.6319586 0.1121207 0.6240896 0.1390262 0.6300215 0.1224703 0.6384375 0.1309151 0.6186115 0.1109533 0.6300215 0.1224703 0.6196037 0.1242463 0.6463179 0.05206131 0.6294717 0.04620414 0.6380097 0.03752434 0.6179833 0.05785548 0.6294717 0.04620414 0.6313902 0.05672484 0.6233487 0.02949005 0.6294717 0.04620414 0.6189266 0.04444664 0.5587999 0.03054726 0.55329 0.04660743 0.5450069 0.03846722 0.5647277 0.0578795 0.55329 0.04660743 0.563474 0.04464906 0.537357 0.05238252 0.55329 0.04660743 0.5514918 0.05681538 0.537395 0.1165966 0.5538397 0.1224506 0.54545 0.1308104 0.5653417 0.1110531 0.5538397 0.1224506 0.5520683 0.1120904 0.5595292 0.1386324 0.5538397 0.1224506 0.564132 0.1243813 0.7364018 0.2361788 0.5734679 0.2546974 0.5702925 0.2408316 0.7339112 0.2504816 0.5742035 0.2677037 0.5734679 0.2546974 0.7597498 0.2586472 0.7451931 0.428944 0.7452447 0.2615504 0.7452447 0.2615504 0.7314437 0.4281437 0.7335389 0.2620936 0.5666657 0.4610032 0.732751 0.4417741 0.7362783 0.4560214 0.5695233 0.446253 0.7314437 0.4281437 0.732751 0.4417741 0.5471693 0.2637142 0.5573531 0.4344296 0.5429396 0.4374279 0.5618067 0.2668063 0.5704158 0.434005 0.5573531 0.4344296 0.6186115 0.1109533 0.6313902 0.05672484 0.6319586 0.1121207 0.6319586 0.1121207 0.6463179 0.05206131 0.646318 0.1167042 0.5647277 0.0578795 0.6189266 0.04444664 0.6179833 0.05785548 0.563474 0.04464906 0.6233487 0.02949005 0.6189266 0.04444664 0.5647277 0.0578795 0.5520683 0.1120904 0.5514918 0.05681538 0.5514918 0.05681538 0.537395 0.1165966 0.537357 0.05238252 0.5653417 0.1110531 0.6196037 0.1242463 0.564132 0.1243813 0.564132 0.1243813 0.6240896 0.1390262 0.5595292 0.1386324 0.920629 0.6707581 0.9347499 0.2272918 0.9335129 0.6714929 0.9347499 0.2272918 0.9463997 0.670827 0.9335129 0.6714929 0.5670242 0.965082 0.555373 0.5215635 0.5682703 0.5209023 0.555373 0.5215635 0.5412353 0.9650067 0.5424789 0.5208275 0.6337562 0.9649829 0.6478716 0.5215623 0.6466588 0.9657142 0.6478716 0.5215623 0.6595652 0.9650505 0.6466588 0.9657142 0.8538994 0.670836 0.8422524 0.2272896 0.8551378 0.2266262 0.8422524 0.2272896 0.8281325 0.6707614 0.8293721 0.2265521 0.7597498 0.2586472 0.7703812 0.4320634 0.759702 0.4320898 0.8293721 0.2265521 0.8191422 0.6698262 0.8216365 0.22605 0.6607772 0.5208992 0.6685189 0.9641739 0.6595652 0.9650505 0.6233487 0.02949005 0.5587301 0.02542001 0.6232791 0.02438056 0.7335389 0.2620936 0.5865367 0.2754362 0.5742035 0.2677037 0.5704158 0.434005 0.7223314 0.4201838 0.7314437 0.4281437 0.7314437 0.4281437 0.7237446 0.2732378 0.7335389 0.2620936 0.5742035 0.2677037 0.5785265 0.4229285 0.5704158 0.434005 0.7364018 0.2361788 0.5700052 0.2305752 0.7361146 0.2259224 0.5682703 0.5209023 0.5760124 0.9641988 0.5670242 0.965082 0.6463179 0.05206131 0.6514748 0.1167042 0.646318 0.1167042 0.6349689 0.520831 0.6247736 0.9640501 0.6272342 0.5203303 0.9218659 0.2265568 0.9116415 0.6698235 0.9141306 0.2260552 0.7362783 0.4560214 0.5669806 0.4714367 0.5666657 0.4610032 0.8551378 0.2266262 0.8628804 0.6699542 0.8538994 0.670836 0.537357 0.05238252 0.5322986 0.1165956 0.5322657 0.05238139 0.9476367 0.2266263 0.9553791 0.6699452 0.9463997 0.670827 0.5471693 0.2637142 0.5322717 0.4371595 0.5365153 0.2634462 0.5424789 0.5208275 0.5322717 0.9640774 0.5347437 0.5203289 0.6240896 0.1390262 0.5595021 0.1437476 0.5595292 0.1386324 0.02438068 0.4274392 0.4835041 0.533547 0.02438056 0.5800693 0.9756194 0.8766642 0.8192679 0.7205649 0.9756194 0.7202605 0.4835104 0.6288304 0.02438056 0.7345703 0.02438056 0.6750887 0.02438056 0.378678 0.4835105 0.2724065 0.4835104 0.3322243 0.02438056 0.1771612 0.4835045 0.07090485 0.4835045 0.1307352 0.2347516 0.8773799 0.2744972 0.871675 0.2769502 0.895384 0.2276332 0.8173105 0.2657305 0.8151974 0.2720444 0.8479661 0.2382609 0.9014936 0.2769502 0.895384 0.2832639 0.9281525 0.2324163 0.8524765 0.2720444 0.8479661 0.2744972 0.871675 0.2832639 0.9281525 0.2769502 0.895384 0.316578 0.8908735 0.2720444 0.8479661 0.3107335 0.8418564 0.3142428 0.8659701 0.2744972 0.871675 0.3142428 0.8659701 0.316578 0.8908735 0.2657305 0.8151974 0.3021747 0.81093 0.3107335 0.8418564 0.316578 0.8908735 0.3607792 0.8871456 0.3617075 0.9264094 0.3107335 0.8418564 0.3511369 0.8312228 0.3581165 0.8580409 0.3142428 0.8659701 0.3581165 0.8580409 0.3607792 0.8871456 0.3107335 0.8418564 0.3021747 0.81093 0.337064 0.8023198 0.3617075 0.9264094 0.3607792 0.8871456 0.4242356 0.8883687 0.3511369 0.8312228 0.3993613 0.8057134 0.4212717 0.8423786 0.3607792 0.8871456 0.3581165 0.8580409 0.4212717 0.8423786 0.337064 0.8023198 0.3679613 0.7822087 0.3993613 0.8057134 0.1496332 0.9376367 0.1978574 0.9121273 0.2119304 0.9410302 0.1247587 0.8549814 0.1882152 0.8562045 0.1908779 0.8853091 0.1496332 0.9376367 0.1277227 0.9009717 0.1908779 0.8853091 0.1247587 0.8549814 0.1442257 0.8095122 0.1872869 0.8169406 0.1882152 0.8562045 0.2324163 0.8524765 0.2347516 0.8773799 0.1908779 0.8853091 0.2347516 0.8773799 0.2382609 0.9014936 0.1872869 0.8169406 0.2276332 0.8173105 0.2324163 0.8524765 0.2119304 0.9410302 0.1978574 0.9121273 0.2382609 0.9014936 0.5653417 0.1110531 0.5647277 0.0578795 0.6179833 0.05785548 0.5702925 0.2408316 0.5734679 0.2546974 0.5639945 0.2571572 0.5742035 0.2677037 0.5618067 0.2668063 0.5639945 0.2571572 0.5471693 0.2637142 0.5562849 0.2494693 0.5639945 0.2571572 0.7364018 0.2361788 0.7506223 0.2446186 0.7431017 0.2523872 0.7597498 0.2586472 0.7452447 0.2615504 0.7431017 0.2523872 0.7335389 0.2620936 0.7339112 0.2504816 0.7431017 0.2523872 0.759702 0.4320898 0.7507525 0.446845 0.7427436 0.4390336 0.7362783 0.4560214 0.732751 0.4417741 0.7427436 0.4390336 0.7314437 0.4281437 0.7451931 0.428944 0.7427436 0.4390336 0.5666657 0.4610032 0.5519188 0.4520102 0.5597555 0.4440929 0.5429396 0.4374279 0.5573531 0.4344296 0.5597555 0.4440929 0.5704158 0.434005 0.5695233 0.446253 0.5597555 0.4440929 0.646318 0.1167042 0.6384375 0.1309151 0.6300215 0.1224703 0.6240896 0.1390262 0.6196037 0.1242463 0.6300215 0.1224703 0.6186115 0.1109533 0.6319586 0.1121207 0.6300215 0.1224703 0.6463179 0.05206131 0.6313902 0.05672484 0.6294717 0.04620414 0.6179833 0.05785548 0.6189266 0.04444664 0.6294717 0.04620414 0.6233487 0.02949005 0.6380097 0.03752434 0.6294717 0.04620414 0.5587999 0.03054726 0.563474 0.04464906 0.55329 0.04660743 0.5647277 0.0578795 0.5514918 0.05681538 0.55329 0.04660743 0.537357 0.05238252 0.5450069 0.03846722 0.55329 0.04660743 0.537395 0.1165966 0.5520683 0.1120904 0.5538397 0.1224506 0.5653417 0.1110531 0.564132 0.1243813 0.5538397 0.1224506 0.5595292 0.1386324 0.54545 0.1308104 0.5538397 0.1224506 0.7364018 0.2361788 0.7339112 0.2504816 0.5734679 0.2546974 0.7339112 0.2504816 0.7335389 0.2620936 0.5742035 0.2677037 0.7597498 0.2586472 0.759702 0.4320898 0.7451931 0.428944 0.7452447 0.2615504 0.7451931 0.428944 0.7314437 0.4281437 0.5666657 0.4610032 0.5695233 0.446253 0.732751 0.4417741 0.5695233 0.446253 0.5704158 0.434005 0.7314437 0.4281437 0.5471693 0.2637142 0.5618067 0.2668063 0.5573531 0.4344296 0.5618067 0.2668063 0.5742035 0.2677037 0.5704158 0.434005 0.6186115 0.1109533 0.6179833 0.05785548 0.6313902 0.05672484 0.6319586 0.1121207 0.6313902 0.05672484 0.6463179 0.05206131 0.5647277 0.0578795 0.563474 0.04464906 0.6189266 0.04444664 0.563474 0.04464906 0.5587999 0.03054726 0.6233487 0.02949005 0.5647277 0.0578795 0.5653417 0.1110531 0.5520683 0.1120904 0.5514918 0.05681538 0.5520683 0.1120904 0.537395 0.1165966 0.5653417 0.1110531 0.6186115 0.1109533 0.6196037 0.1242463 0.564132 0.1243813 0.6196037 0.1242463 0.6240896 0.1390262 0.920629 0.6707581 0.9218659 0.2265568 0.9347499 0.2272918 0.9347499 0.2272918 0.9476367 0.2266263 0.9463997 0.670827 0.5670242 0.965082 0.5541267 0.9657429 0.555373 0.5215635 0.555373 0.5215635 0.5541267 0.9657429 0.5412353 0.9650067 0.6337562 0.9649829 0.6349689 0.520831 0.6478716 0.5215623 0.6478716 0.5215623 0.6607772 0.5208992 0.6595652 0.9650505 0.8538994 0.670836 0.8410139 0.6714993 0.8422524 0.2272896 0.8422524 0.2272896 0.8410139 0.6714993 0.8281325 0.6707614 0.7597498 0.2586472 0.7703812 0.2586208 0.7703812 0.4320634 0.8293721 0.2265521 0.8281325 0.6707614 0.8191422 0.6698262 0.6607772 0.5208992 0.6685189 0.5201979 0.6685189 0.9641739 0.6233487 0.02949005 0.5587999 0.03054726 0.5587301 0.02542001 0.7335389 0.2620936 0.7236471 0.2729968 0.5865367 0.2754362 0.5704158 0.434005 0.5787408 0.4229991 0.7223314 0.4201838 0.7314437 0.4281437 0.7224587 0.4200533 0.7237446 0.2732378 0.5742035 0.2677037 0.5864794 0.2756844 0.5785265 0.4229285 0.7364018 0.2361788 0.5702925 0.2408316 0.5700052 0.2305752 0.5682703 0.5209023 0.5760124 0.5201979 0.5760124 0.9641988 0.6463179 0.05206131 0.6514748 0.05206131 0.6514748 0.1167042 0.6349689 0.520831 0.6337562 0.9649829 0.6247736 0.9640501 0.9218659 0.2265568 0.920629 0.6707581 0.9116415 0.6698235 0.7362783 0.4560214 0.7365928 0.4664415 0.5669806 0.4714367 0.8551378 0.2266262 0.8628804 0.2259224 0.8628804 0.6699542 0.537357 0.05238252 0.537395 0.1165966 0.5322986 0.1165956 0.9476367 0.2266263 0.9553791 0.2259224 0.9553791 0.6699452 0.5471693 0.2637142 0.5429396 0.4374279 0.5322717 0.4371595 0.5424789 0.5208275 0.5412353 0.9650067 0.5322717 0.9640774 0.6240896 0.1390262 0.6240625 0.1441362 0.5595021 0.1437476 0.02438068 0.4274392 0.4835041 0.4737922 0.4835041 0.533547 0.9756194 0.8766642 0.8191422 0.8764122 0.8192679 0.7205649 0.4835104 0.6288304 0.4835104 0.7809193 0.02438056 0.7345703 0.02438056 0.378678 0.02438068 0.2259224 0.4835105 0.2724065 0.02438056 0.1771612 0.02438056 0.02438056 0.4835045 0.07090485 + + + + + + + + + + + + + + +

21 0 0 4 1 1 10 2 2 2 3 3 9 4 4 3 5 5 0 6 6 7 7 7 1 8 8 3 5 9 10 2 10 4 1 11 1 8 12 8 9 13 2 3 14 0 6 15 5 10 16 6 11 17 10 2 18 14 12 19 15 13 20 7 7 21 13 14 22 8 9 23 6 11 24 5 10 25 11 15 26 21 0 27 10 2 28 15 13 29 8 9 30 14 12 31 9 4 32 6 11 33 12 16 34 7 7 35 14 12 36 20 17 37 15 13 38 12 16 39 18 18 40 13 14 41 11 15 42 5 10 43 16 19 44 21 0 45 15 13 46 20 17 47 13 14 48 19 20 49 14 12 50 12 16 51 16 19 52 17 21 53 20 17 54 25 22 55 26 23 56 17 21 57 24 24 58 18 18 59 16 19 60 5 10 61 22 25 62 21 0 63 20 17 64 26 23 65 19 20 66 24 24 67 25 22 68 16 19 69 23 26 70 17 21 71 25 22 72 31 27 73 26 23 74 23 26 75 29 28 76 24 24 77 22 25 78 5 10 79 27 29 80 21 0 81 26 23 82 31 27 83 25 22 84 29 28 85 30 30 86 23 26 87 27 29 88 28 31 89 28 31 90 2 3 91 29 28 92 27 29 93 5 10 94 0 6 95 21 0 96 31 27 97 4 1 98 29 28 99 3 5 100 30 30 101 27 29 102 1 8 103 28 31 104 31 27 105 3 5 106 4 1 107 73 32 108 60 33 109 55 34 110 32 35 111 35 36 112 37 37 113 33 38 114 35 36 115 34 39 116 98 40 117 35 36 118 36 41 119 82 42 120 40 43 121 42 44 122 80 45 123 40 43 124 39 46 125 38 47 126 40 43 127 41 48 128 43 49 129 45 50 130 47 51 131 93 52 132 45 50 133 44 53 134 81 54 135 45 50 136 46 55 137 48 56 138 52 57 139 54 58 140 49 59 141 52 57 142 51 60 143 50 61 144 52 57 145 53 62 146 83 63 147 57 64 148 59 65 149 99 66 150 57 64 151 56 67 152 55 34 153 57 64 154 58 68 155 84 69 156 62 70 157 64 71 158 60 33 159 62 70 160 61 72 161 79 73 162 62 70 163 63 74 164 65 75 165 69 76 166 71 77 167 66 78 168 69 76 169 68 79 170 67 80 171 69 76 172 70 81 173 72 82 174 76 83 175 78 84 176 73 32 177 76 83 178 75 85 179 74 86 180 76 83 181 77 87 182 82 42 183 34 39 184 32 35 185 42 44 186 33 38 187 34 39 188 80 45 189 47 51 190 41 48 191 41 48 192 81 54 193 38 47 194 48 56 195 46 55 196 93 52 197 54 58 198 81 54 199 46 55 200 98 40 201 53 62 202 49 59 203 36 41 204 50 61 205 53 62 206 55 34 207 61 72 208 59 65 209 59 65 210 84 69 211 83 63 212 66 78 213 63 74 214 60 33 215 68 79 216 79 73 217 63 74 218 66 78 219 75 85 220 70 81 221 70 81 222 72 82 223 67 80 224 73 32 225 58 68 226 77 87 227 77 87 228 99 66 229 74 86 230 48 56 231 78 84 232 51 60 233 78 84 234 49 59 235 51 60 236 32 35 237 56 67 238 83 63 239 56 67 240 98 40 241 99 66 242 82 42 243 64 71 244 39 46 245 64 71 246 80 45 247 39 46 248 93 52 249 71 77 250 67 80 251 71 77 252 43 49 253 65 75 254 80 0 255 90 0 256 43 0 257 65 88 258 90 88 259 91 88 260 79 89 261 89 89 262 80 89 263 79 90 264 91 90 265 92 90 266 38 91 267 105 91 268 33 91 269 50 92 270 106 92 271 81 92 272 81 93 273 104 93 274 38 93 275 33 94 276 107 94 277 50 94 278 82 0 279 85 0 280 86 0 281 83 95 282 85 95 283 32 95 284 84 10 285 88 10 286 83 10 287 84 96 288 86 96 289 87 96 290 72 97 291 95 97 292 96 97 293 93 0 294 95 0 295 48 0 296 67 98 297 94 98 298 93 98 299 67 10 300 96 10 301 97 10 302 74 99 303 100 99 304 49 99 305 98 100 306 100 100 307 101 100 308 99 101 309 101 101 310 102 101 311 99 102 312 103 102 313 74 102 314 112 103 315 114 103 316 113 103 317 127 104 318 124 104 319 126 104 320 109 105 321 111 105 322 110 105 323 117 106 324 119 106 325 118 106 326 121 107 327 123 107 328 122 107 329 2 3 330 8 9 331 9 4 332 0 6 333 6 11 334 7 7 335 3 5 336 9 4 337 10 2 338 1 8 339 7 7 340 8 9 341 10 2 342 9 4 343 14 12 344 7 7 345 12 16 346 13 14 347 8 9 348 13 14 349 14 12 350 6 11 351 11 15 352 12 16 353 14 12 354 19 20 355 20 17 356 12 16 357 17 21 358 18 18 359 13 14 360 18 18 361 19 20 362 12 16 363 11 15 364 16 19 365 20 17 366 19 20 367 25 22 368 17 21 369 23 26 370 24 24 371 19 20 372 18 18 373 24 24 374 16 19 375 22 25 376 23 26 377 25 22 378 30 30 379 31 27 380 23 26 381 28 31 382 29 28 383 25 22 384 24 24 385 29 28 386 23 26 387 22 25 388 27 29 389 28 31 390 1 8 391 2 3 392 29 28 393 2 3 394 3 5 395 27 29 396 0 6 397 1 8 398 31 27 399 30 30 400 3 5 401 73 32 402 66 78 403 60 33 404 32 35 405 34 39 406 35 36 407 33 38 408 36 41 409 35 36 410 98 40 411 37 37 412 35 36 413 82 42 414 39 46 415 40 43 416 80 45 417 41 48 418 40 43 419 38 47 420 42 44 421 40 43 422 43 49 423 44 53 424 45 50 425 93 52 426 46 55 427 45 50 428 81 54 429 47 51 430 45 50 431 48 56 432 51 60 433 52 57 434 49 59 435 53 62 436 52 57 437 50 61 438 54 58 439 52 57 440 83 63 441 56 67 442 57 64 443 99 66 444 58 68 445 57 64 446 55 34 447 59 65 448 57 64 449 84 69 450 61 72 451 62 70 452 60 33 453 63 74 454 62 70 455 79 73 456 64 71 457 62 70 458 65 75 459 68 79 460 69 76 461 66 78 462 70 81 463 69 76 464 67 80 465 71 77 466 69 76 467 72 82 468 75 85 469 76 83 470 73 32 471 77 87 472 76 83 473 74 86 474 78 84 475 76 83 476 82 42 477 42 44 478 34 39 479 42 44 480 38 47 481 33 38 482 80 45 483 43 49 484 47 51 485 41 48 486 47 51 487 81 54 488 48 56 489 54 58 490 46 55 491 54 58 492 50 61 493 81 54 494 98 40 495 36 41 496 53 62 497 36 41 498 33 38 499 50 61 500 55 34 501 60 33 502 61 72 503 59 65 504 61 72 505 84 69 506 66 78 507 68 79 508 63 74 509 68 79 510 65 75 511 79 73 512 66 78 513 73 32 514 75 85 515 70 81 516 75 85 517 72 82 518 73 32 519 55 34 520 58 68 521 77 87 522 58 68 523 99 66 524 48 56 525 72 82 526 78 84 527 78 84 528 74 86 529 49 59 530 32 35 531 37 37 532 56 67 533 56 67 534 37 37 535 98 40 536 82 42 537 84 69 538 64 71 539 64 71 540 79 73 541 80 45 542 93 52 543 44 53 544 71 77 545 71 77 546 44 53 547 43 49 548 80 108 549 89 108 550 90 108 551 65 109 552 43 109 553 90 109 554 79 110 555 92 110 556 89 110 557 79 111 558 65 111 559 91 111 560 38 91 561 104 91 562 105 91 563 50 112 564 107 112 565 106 112 566 81 113 567 106 113 568 104 113 569 33 114 570 105 114 571 107 114 572 82 0 573 32 0 574 85 0 575 83 115 576 88 115 577 85 115 578 84 10 579 87 10 580 88 10 581 84 116 582 82 116 583 86 116 584 72 117 585 48 117 586 95 117 587 93 0 588 94 0 589 95 0 590 67 118 591 97 118 592 94 118 593 67 10 594 72 10 595 96 10 596 74 119 597 103 119 598 100 119 599 98 0 600 49 0 601 100 0 602 99 120 603 98 120 604 101 120 605 99 121 606 102 121 607 103 121 608 112 122 609 115 122 610 114 122 611 127 10 612 125 10 613 124 10 614 109 123 615 108 123 616 111 123 617 117 124 618 116 124 619 119 124 620 121 125 621 120 125 622 123 125 623

+
+
+
+
+ + + + 2 0 0 0 0 2 0 0 0 0 2 0 0 0 0 1 + + ping-marker-ball ping-marker-body base + + + + + + + + 1 0 0 2.51159e-5 0 0 1 -1.13652 0 -1 0 -1.02893e-5 0 0 0 1 1 0 0 -1.92262e-5 0 0 1 -5.238245 0 -1 0 7.9833e-6 0 0 0 1 1 0 0 0 0 0 1 0 0 -1 0 0 0 0 0 1 + + + + + + + + 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 + + + + + + + + + + + + + + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 0 0 2 1 0 2 2 3 0 4 2 5 0 6 2 7 0 8 2 9 0 10 2 11 0 12 2 13 0 14 2 15 0 16 2 17 0 18 2 19 0 20 2 21 0 22 2 23 0 24 2 25 0 26 2 27 0 28 2 29 0 30 2 31 0 32 2 33 0 34 2 35 0 36 2 37 0 38 2 39 0 40 2 41 0 42 2 43 0 44 2 45 0 46 2 47 0 48 2 49 0 50 2 51 0 52 2 53 0 54 2 55 0 56 2 57 0 58 2 59 0 60 2 61 0 62 2 63 1 64 2 65 1 66 2 67 1 68 2 69 1 70 2 71 1 72 2 73 1 74 2 75 1 76 2 77 1 78 2 79 1 80 2 81 1 82 2 83 1 84 2 85 1 86 2 87 1 88 2 89 1 90 2 91 1 92 2 93 1 94 2 95 1 96 2 97 1 98 2 99 1 100 2 101 1 102 2 103 1 104 2 105 1 106 2 107 1 108 2 109 1 110 2 111 1 112 2 113 1 114 2 115 1 116 2 117 1 118 2 119 1 120 2 121 1 122 2 123 1 124 2 125 1 126 2 127 1 128 2 129 1 130 2 131 1 132 2 133 1 134 2 135 1 136 2 137 1 138 2 139 1 140 2 141 1 142 2 143 1 144 2 145 1 146 2 147 1 148 2 149 1 150 2 151 1 152 2 153 1 154 2 155 1 156 2 157 1 158 2 159 1 160 2 161 1 162 2 163 1 164 2 165 1 166 2 167 1 168 2 169 1 170 2 171 1 172 2 173 1 174 2 175 1 176 2 177 1 178 2 179 1 180 2 181 1 182 2 183 1 184 2 185 1 186 2 187 1 188 2 189 1 190 2 191 1 192 2 193 1 194 2 195 1 196 2 197 1 198 2 199 1 200 2 201 1 202 2 203 1 204 2 205 1 206 2 207 1 208 2 209 1 210 2 211 1 212 2 213 1 214 2 215 1 216 2 217 1 218 2 219 1 220 2 221 1 222 2 223 1 224 2 225 1 226 2 227 1 228 2 229 1 230 2 231 1 232 2 233 1 234 2 235 1 236 2 237 1 238 2 239 1 240 2 241 1 242 2 243 1 244 2 245 1 246 2 247 1 248 2 249 1 250 2 251 1 252 2 253 1 254 2 255 + + + + + + + + + 0 0.04166662 0.08333331 0.125 0.1666666 0.2083333 0.25 0.2916666 0.3333333 0.375 0.4166666 0.4583333 0.5 0.5416667 0.5833333 0.625 0.6666667 0.7083333 0.75 0.7916667 0.8333333 0.875 0.9166667 0.9583333 1 1.041667 1.083333 1.125 1.166667 1.208333 1.25 1.291667 1.333333 1.375 1.416667 1.458333 1.5 1.541667 1.583333 1.625 1.666667 1.708333 1.75 1.791667 1.833333 1.875 1.916667 1.958333 2 2.041667 2.083333 2.125 2.166667 2.208333 2.25 2.291667 2.333333 2.375 2.416667 2.458333 2.5 2.541667 2.583333 2.625 2.666667 2.708333 2.75 2.791667 2.833333 2.875 2.916667 2.958333 3 3.041667 3.083333 3.125 3.166667 3.208333 3.25 3.291667 3.333333 3.375 3.416667 3.458333 3.5 3.541667 3.583333 3.625 3.666667 3.708333 3.75 3.791667 3.833333 3.875 3.916667 3.958333 4 4.041666 4.083333 4.125 4.166666 + + + + + + + + 1 0 0 0 0 0 -1 0 0 1 0 0 0 0 0 1 0.9983632 0 0.05719231 0 0.05719231 0 -0.9983632 0 0 1 0 0 0 0 0 1 0.9933116 0 0.1154642 0 0.1154642 0 -0.9933116 0 0 1 0 0 0 0 0 1 0.9846505 0 0.1745377 0 0.1745377 0 -0.9846505 0 0 1 0 0 0 0 0 1 0.9722123 0 0.234101 0 0.234101 0 -0.9722123 0 0 1 0 0 0 0 0 1 0.9558632 0 0.293812 0 0.293812 0 -0.9558632 0 0 1 0 0 0 0 0 1 0.9355093 0 0.353302 0 0.353302 0 -0.9355093 0 0 1 0 0 0 0 0 1 0.9111016 0 0.4121819 0 0.4121819 0 -0.9111016 0 0 1 0 0 0 0 0 1 0.8826405 0 0.4700486 0 0.4700486 0 -0.8826405 0 0 1 0 0 0 0 0 1 0.8501794 0 0.5264931 0 0.5264931 0 -0.8501794 0 0 1 0 0 0 0 0 1 0.813826 0 0.5811087 0 0.5811087 0 -0.813826 0 0 1 0 0 0 0 0 1 0.7737424 0 0.6335003 0 0.6335003 0 -0.7737424 0 0 1 0 0 0 0 0 1 0.7301437 0 0.6832936 0 0.6832936 0 -0.7301437 0 0 1 0 0 0 0 0 1 0.6832936 0 0.7301437 0 0.7301437 0 -0.6832936 0 0 1 0 0 0 0 0 1 0.6335002 0 0.7737426 0 0.7737426 0 -0.6335002 0 0 1 0 0 0 0 0 1 0.5811086 0 0.813826 0 0.813826 0 -0.5811086 0 0 1 0 0 0 0 0 1 0.526493 0 0.8501795 0 0.8501795 0 -0.526493 0 0 1 0 0 0 0 0 1 0.4700487 0 0.8826405 0 0.8826405 0 -0.4700487 0 0 1 0 0 0 0 0 1 0.412182 0 0.9111015 0 0.9111015 0 -0.412182 0 0 1 0 0 0 0 0 1 0.353302 0 0.9355093 0 0.9355093 0 -0.353302 0 0 1 0 0 0 0 0 1 0.2938119 0 0.9558633 0 0.9558633 0 -0.2938119 0 0 1 0 0 0 0 0 1 0.2341012 0 0.9722121 0 0.9722121 0 -0.2341012 0 0 1 0 0 0 0 0 1 0.1745378 0 0.9846504 0 0.9846504 0 -0.1745378 0 0 1 0 0 0 0 0 1 0.1154642 0 0.9933116 0 0.9933116 0 -0.1154642 0 0 1 0 0 0 0 0 1 0.05719256 0 0.998363 0 0.998363 0 -0.05719256 0 0 1 0 0 0 0 0 1 -1.34359e-7 0 1 0 1 0 1.34359e-7 0 0 1 0 0 0 0 0 1 -0.05719216 0 0.9983631 0 0.9983631 0 0.05719216 0 0 1 0 0 0 0 0 1 -0.1154643 0 0.9933117 0 0.9933117 0 0.1154643 0 0 1 0 0 0 0 0 1 -0.1745376 0 0.9846504 0 0.9846504 0 0.1745376 0 0 1 0 0 0 0 0 1 -0.2341008 0 0.9722121 0 0.9722121 0 0.2341008 0 0 1 0 0 0 0 0 1 -0.293812 0 0.9558632 0 0.9558632 0 0.293812 0 0 1 0 0 0 0 0 1 -0.3533019 0 0.9355093 0 0.9355093 0 0.3533019 0 0 1 0 0 0 0 0 1 -0.412182 0 0.9111015 0 0.9111015 0 0.412182 0 0 1 0 0 0 0 0 1 -0.4700488 0 0.8826405 0 0.8826405 0 0.4700488 0 0 1 0 0 0 0 0 1 -0.5264931 0 0.8501795 0 0.8501795 0 0.5264931 0 0 1 0 0 0 0 0 1 -0.5811087 0 0.8138261 0 0.8138261 0 0.5811087 0 0 1 0 0 0 0 0 1 -0.6335001 0 0.7737424 0 0.7737424 0 0.6335001 0 0 1 0 0 0 0 0 1 -0.6832936 0 0.7301437 0 0.7301437 0 0.6832936 0 0 1 0 0 0 0 0 1 -0.7301437 0 0.6832936 0 0.6832936 0 0.7301437 0 0 1 0 0 0 0 0 1 -0.7737427 0 0.6335003 0 0.6335003 0 0.7737427 0 0 1 0 0 0 0 0 1 -0.813826 0 0.5811086 0 0.5811086 0 0.813826 0 0 1 0 0 0 0 0 1 -0.8501792 0 0.5264929 0 0.5264929 0 0.8501792 0 0 1 0 0 0 0 0 1 -0.8826406 0 0.4700486 0 0.4700486 0 0.8826406 0 0 1 0 0 0 0 0 1 -0.9111014 0 0.4121819 0 0.4121819 0 0.9111014 0 0 1 0 0 0 0 0 1 -0.9355093 0 0.3533019 0 0.3533019 0 0.9355093 0 0 1 0 0 0 0 0 1 -0.9558632 0 0.2938119 0 0.2938119 0 0.9558632 0 0 1 0 0 0 0 0 1 -0.972212 0 0.234101 0 0.234101 0 0.972212 0 0 1 0 0 0 0 0 1 -0.9846506 0 0.1745377 0 0.1745377 0 0.9846506 0 0 1 0 0 0 0 0 1 -0.9933118 0 0.1154643 0 0.1154643 0 0.9933118 0 0 1 0 0 0 0 0 1 -0.9983631 0 0.05719225 0 0.05719225 0 0.9983631 0 0 1 0 0 0 0 0 1 -1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 -0.9983629 0 -0.05719229 0 -0.05719229 0 0.9983629 0 0 1 0 0 0 0 0 1 -0.9933118 0 -0.1154642 0 -0.1154642 0 0.9933118 0 0 1 0 0 0 0 0 1 -0.9846506 0 -0.1745377 0 -0.1745377 0 0.9846506 0 0 1 0 0 0 0 0 1 -0.9722125 0 -0.234101 0 -0.234101 0 0.9722125 0 0 1 0 0 0 0 0 1 -0.9558632 0 -0.2938119 0 -0.2938119 0 0.9558632 0 0 1 0 0 0 0 0 1 -0.9355093 0 -0.353302 0 -0.353302 0 0.9355093 0 0 1 0 0 0 0 0 1 -0.9111014 0 -0.4121819 0 -0.4121819 0 0.9111014 0 0 1 0 0 0 0 0 1 -0.8826404 0 -0.4700486 0 -0.4700486 0 0.8826404 0 0 1 0 0 0 0 0 1 -0.8501792 0 -0.526493 0 -0.526493 0 0.8501792 0 0 1 0 0 0 0 0 1 -0.813826 0 -0.5811085 0 -0.5811085 0 0.813826 0 0 1 0 0 0 0 0 1 -0.7737426 0 -0.6335003 0 -0.6335003 0 0.7737426 0 0 1 0 0 0 0 0 1 -0.7301437 0 -0.6832937 0 -0.6832937 0 0.7301437 0 0 1 0 0 0 0 0 1 -0.6832939 0 -0.7301438 0 -0.7301438 0 0.6832939 0 0 1 0 0 0 0 0 1 -0.6335003 0 -0.7737425 0 -0.7737425 0 0.6335003 0 0 1 0 0 0 0 0 1 -0.5811086 0 -0.813826 0 -0.813826 0 0.5811086 0 0 1 0 0 0 0 0 1 -0.5264929 0 -0.8501794 0 -0.8501794 0 0.5264929 0 0 1 0 0 0 0 0 1 -0.4700485 0 -0.8826405 0 -0.8826405 0 0.4700485 0 0 1 0 0 0 0 0 1 -0.4121818 0 -0.9111015 0 -0.9111015 0 0.4121818 0 0 1 0 0 0 0 0 1 -0.3533017 0 -0.9355092 0 -0.9355092 0 0.3533017 0 0 1 0 0 0 0 0 1 -0.2938122 0 -0.9558634 0 -0.9558634 0 0.2938122 0 0 1 0 0 0 0 0 1 -0.2341008 0 -0.9722122 0 -0.9722122 0 0.2341008 0 0 1 0 0 0 0 0 1 -0.1745378 0 -0.9846506 0 -0.9846506 0 0.1745378 0 0 1 0 0 0 0 0 1 -0.1154643 0 -0.9933116 0 -0.9933116 0 0.1154643 0 0 1 0 0 0 0 0 1 -0.05719216 0 -0.9983631 0 -0.9983631 0 0.05719216 0 0 1 0 0 0 0 0 1 -1.34359e-7 0 -1 0 -1 0 1.34359e-7 0 0 1 0 0 0 0 0 1 0.0570372 0 -0.9983721 0 -0.9983721 0 -0.0570372 0 0 1 0 0 0 0 0 1 0.1151472 0 -0.9933485 0 -0.9933485 0 -0.1151472 0 0 1 0 0 0 0 0 1 0.1740543 0 -0.9847361 0 -0.9847361 0 -0.1740543 0 0 1 0 0 0 0 0 1 0.2334493 0 -0.9723691 0 -0.9723691 0 -0.2334493 0 0 1 0 0 0 0 0 1 0.2929935 0 -0.9561144 0 -0.9561144 0 -0.2929935 0 0 1 0 0 0 0 0 1 0.3523213 0 -0.9358791 0 -0.9358791 0 -0.3523213 0 0 1 0 0 0 0 0 1 0.4110473 0 -0.911614 0 -0.911614 0 -0.4110473 0 0 1 0 0 0 0 0 1 0.4687718 0 -0.8833194 0 -0.8833194 0 -0.4687718 0 0 1 0 0 0 0 0 1 0.5250899 0 -0.8510468 0 -0.8510468 0 -0.5250899 0 0 1 0 0 0 0 0 1 0.579598 0 -0.8149026 0 -0.8149026 0 -0.579598 0 0 1 0 0 0 0 0 1 0.6319049 0 -0.7750459 0 -0.7750459 0 -0.6319049 0 0 1 0 0 0 0 0 1 0.6816387 0 -0.731689 0 -0.731689 0 -0.6816387 0 0 1 0 0 0 0 0 1 0.7284566 0 -0.685092 0 -0.685092 0 -0.7284566 0 0 1 0 0 0 0 0 1 0.7720523 0 -0.635559 0 -0.635559 0 -0.7720523 0 0 1 0 0 0 0 0 1 0.8121629 0 -0.5834306 0 -0.5834306 0 -0.8121629 0 0 1 0 0 0 0 0 1 0.8485736 0 -0.5290774 0 -0.5290774 0 -0.8485736 0 0 1 0 0 0 0 0 1 0.8811216 0 -0.4728898 0 -0.4728898 0 -0.8811216 0 0 1 0 0 0 0 0 1 0.9096981 0 -0.4152703 0 -0.4152703 0 -0.9096981 0 0 1 0 0 0 0 0 1 0.9342478 0 -0.3566245 0 -0.3566245 0 -0.9342478 0 0 1 0 0 0 0 0 1 0.9547679 0 -0.2973522 0 -0.2973522 0 -0.9547679 0 0 1 0 0 0 0 0 1 0.9713044 0 -0.2378396 0 -0.2378396 0 -0.9713044 0 0 1 0 0 0 0 0 1 0.9839484 0 -0.1784533 0 -0.1784533 0 -0.9839484 0 0 1 0 0 0 0 0 1 0.9928301 0 -0.119534 0 -0.119534 0 -0.9928301 0 0 1 0 0 0 0 0 1 0.9981138 0 -0.06139208 0 -0.06139208 0 -0.9981138 0 0 1 0 0 0 0 0 1 0.9999908 0 -0.004305305 0 -0.004305305 0 -0.9999908 0 0 1 0 0 0 0 0 1 + + + + + + + + LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR + + + + + + + + + + + + + + + + 0 0.04166662 0.08333331 0.125 0.1666666 0.2083333 0.25 0.2916666 0.3333333 0.375 0.4166666 0.4583333 0.5 0.5416667 0.5833333 0.625 0.6666667 0.7083333 0.75 0.7916667 0.8333333 0.875 0.9166667 0.9583333 1 1.041667 1.083333 1.125 1.166667 1.208333 1.25 1.291667 1.333333 1.375 1.416667 1.458333 1.5 1.541667 1.583333 1.625 1.666667 1.708333 1.75 1.791667 1.833333 1.875 1.916667 1.958333 2 2.041667 2.083333 2.125 2.166667 2.208333 2.25 2.291667 2.333333 2.375 2.416667 2.458333 2.5 2.541667 2.583333 2.625 2.666667 2.708333 2.75 2.791667 2.833333 2.875 2.916667 2.958333 3 3.041667 3.083333 3.125 3.166667 3.208333 3.25 3.291667 3.333333 3.375 3.416667 3.458333 3.5 3.541667 3.583333 3.625 3.666667 3.708333 3.75 3.791667 3.833333 3.875 3.916667 3.958333 4 4.041666 4.083333 4.125 4.166666 + + + + + + + + 1 0 0 -2.51159e-5 0 1 0 1.13652 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.151992 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.167464 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.182937 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.198409 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.213881 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.229353 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.244825 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.260297 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.275769 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.291241 0 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.306713 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.322185 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.337657 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 2.98023e-8 -2.51159e-5 0 1 0 1.353129 -2.98023e-8 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.368601 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.384073 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.399545 0 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.415017 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.430489 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.445961 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.461433 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.476906 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.492378 0 0 1 1.02893e-5 0 0 0 1 1 0 -3.72529e-9 -2.51159e-5 0 1 0 1.50785 3.72529e-9 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.523322 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.50785 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.492378 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.476905 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.461433 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.445961 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.430489 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.415017 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.399545 0 0 0.9999999 1.02892e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.384073 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.368601 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.353129 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.337657 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.322185 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.306713 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.291241 0 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.275769 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.260297 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.244825 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.229353 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.213881 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.198409 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.182937 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.167464 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.151992 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.13652 0 0 1 1.02893e-5 0 0 0 1 1 0 3.72529e-9 -2.51159e-5 0 1 0 1.151992 -3.72529e-9 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.167464 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.182937 0 0 1 1.02893e-5 0 0 0 1 1 0 1.49012e-8 -2.51159e-5 0 1 0 1.198409 -1.49012e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.213881 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.229353 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.244825 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.260297 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.275769 0 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.291241 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.306713 2.98023e-8 0 1 1.02892e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.322185 0 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.337657 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.353129 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.368601 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.384073 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.399545 0 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.415017 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 2.98023e-8 -2.51159e-5 0 1 0 1.430489 -2.98023e-8 0 1 1.02893e-5 0 0 0 1 0.9999999 0 -2.98023e-8 -2.51159e-5 0 1 0 1.445961 2.98023e-8 0 0.9999999 1.02892e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.461433 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.476906 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.492378 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.50785 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.523322 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.508202 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.493082 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.477962 0 0 0.9999999 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.462842 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.447722 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.432603 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.417483 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.402363 0 0 0.9999999 1.02892e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.387243 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 -2.98023e-8 -2.51159e-5 0 1 0 1.372123 2.98023e-8 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.357003 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.341883 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.326764 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.311644 0 0 1 1.02892e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.296524 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.281404 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.266284 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.251164 0 0 1 1.02893e-5 0 0 0 1 1 0 -2.98023e-8 -2.51159e-5 0 1 0 1.236044 2.98023e-8 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.220924 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.205805 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.190685 0 0 1 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.175565 0 0 1 1.02893e-5 0 0 0 1 0.9999999 0 0 -2.51159e-5 0 1 0 1.160445 0 0 0.9999999 1.02893e-5 0 0 0 1 1 0 0 -2.51159e-5 0 1 0 1.145325 0 0 1 1.02893e-5 0 0 0 1 + + + + + + + + LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR + + + + + + + + + + + + + + + + 0 0.04166662 0.08333331 0.125 0.1666666 0.2083333 0.25 0.2916666 0.3333333 0.375 0.4166666 0.4583333 0.5 0.5416667 0.5833333 0.625 0.6666667 0.7083333 0.75 0.7916667 0.8333333 0.875 0.9166667 0.9583333 1 1.041667 1.083333 1.125 1.166667 1.208333 1.25 1.291667 1.333333 1.375 1.416667 1.458333 1.5 1.541667 1.583333 1.625 1.666667 1.708333 1.75 1.791667 1.833333 1.875 1.916667 1.958333 2 2.041667 2.083333 2.125 2.166667 2.208333 2.25 2.291667 2.333333 2.375 2.416667 2.458333 2.5 2.541667 2.583333 2.625 2.666667 2.708333 2.75 2.791667 2.833333 2.875 2.916667 2.958333 3 3.041667 3.083333 3.125 3.166667 3.208333 3.25 3.291667 3.333333 3.375 3.416667 3.458333 3.5 3.541667 3.583333 3.625 3.666667 3.708333 3.75 3.791667 3.833333 3.875 3.916667 3.958333 4 4.041666 4.083333 4.125 4.166666 + + + + + + + + 1 0 0 1.92262e-5 0 1 0 5.238245 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.253717 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.269189 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.284661 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.300133 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.315605 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.331077 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.34655 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.362021 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.377493 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.392965 0 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.408438 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.42391 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.439382 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 2.98023e-8 1.92262e-5 0 1 0 5.454854 -2.98023e-8 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.470326 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.485798 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.50127 0 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.516742 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.532214 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.547686 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.563158 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.57863 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.594102 0 0 1 -7.9833e-6 0 0 0 1 1 0 -3.72529e-9 1.92262e-5 0 1 0 5.609574 3.72529e-9 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.625046 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.609574 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.594102 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.57863 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.563158 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.547686 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.532214 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.516742 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.50127 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.485798 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.470326 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.454854 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.439382 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.42391 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.408438 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.392965 0 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.377493 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.362021 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.34655 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.331077 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.315605 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.300133 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.284661 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.269189 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.253717 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.238245 0 0 1 -7.9833e-6 0 0 0 1 1 0 3.72529e-9 1.92262e-5 0 1 0 5.253717 -3.72529e-9 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.269189 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.284661 0 0 1 -7.9833e-6 0 0 0 1 1 0 1.49012e-8 1.92262e-5 0 1 0 5.300133 -1.49012e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.315605 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.331077 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.34655 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.362021 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.377493 0 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.392965 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.408438 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.42391 0 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.439382 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.454854 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.470326 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.485798 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.50127 0 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.516742 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 2.98023e-8 1.92262e-5 0 1 0 5.532214 -2.98023e-8 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 -2.98023e-8 1.92262e-5 0 1 0 5.547686 2.98023e-8 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.563158 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.57863 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.594102 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.609574 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.625046 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.609646 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.594247 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.578847 0 0 0.9999999 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.563447 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.548048 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.532648 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.517248 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.501848 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.486448 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 -2.98023e-8 1.92262e-5 0 1 0 5.471049 2.98023e-8 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.455649 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.440249 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.42485 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.40945 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.39405 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.37865 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.36325 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.34785 0 0 1 -7.9833e-6 0 0 0 1 1 0 -2.98023e-8 1.92262e-5 0 1 0 5.332451 2.98023e-8 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.317051 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.301651 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.286252 0 0 1 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.270852 0 0 1 -7.9833e-6 0 0 0 1 0.9999999 0 0 1.92262e-5 0 1 0 5.255452 0 0 0.9999999 -7.9833e-6 0 0 0 1 1 0 0 1.92262e-5 0 1 0 5.240052 0 0 1 -7.9833e-6 0 0 0 1 + + + + + + + + LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR LINEAR + + + + + + + + + + + + + + + + + + + 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 + + 1 0 0 0 0 0 -1 0 0 1 0 0 0 0 0 1 + + 1 0 0 -2.51159e-5 0 1 0 1.13652 0 0 1 1.02893e-5 0 0 0 1 + + + 0 + 0 + 0 + 6 + + + + + 1 0 0 1.92262e-5 0 1 0 5.238245 0 0 1 -7.9833e-6 0 0 0 1 + + + 0 + 0 + 0 + 6 + + + + + 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 + + + 0 + 0 + 0 + 6 + + + + + + 0 + 0 + 0 + 6 + + + + + 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 + + #ping-marker-armature_base + + + + + + + + + + + + + + + + + + +
Index: ps/trunk/binaries/data/mods/public/art/skeletons/flare-marker-armature.xml =================================================================== --- ps/trunk/binaries/data/mods/public/art/skeletons/flare-marker-armature.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/skeletons/flare-marker-armature.xml (revision 25691) @@ -0,0 +1,27 @@ + + + + + + + + + + + + base + + + base + + ping-marker-ball + + + ping-marker-body + + + + target marker + + + Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame00.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame00.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame00.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame00.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame00.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame01.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame01.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame01.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame01.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame01.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame02.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame02.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame02.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame02.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame02.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame03.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame03.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame03.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame03.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame03.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame04.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame04.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame04.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame04.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame04.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame05.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame05.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame05.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame05.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame05.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame06.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame06.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame06.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame06.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame06.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame07.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame07.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame07.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame07.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame07.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame08.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame08.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame08.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame08.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame08.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame09.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame09.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame09.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame09.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame09.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame10.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame10.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame10.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame10.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame10.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame11.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame11.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame11.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame11.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame11.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame12.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame12.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame12.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame12.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame12.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame13.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame13.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame13.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame13.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame13.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame14.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame14.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame14.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame14.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame14.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame15.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame15.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame15.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame15.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/animated/minimap-flare/frame15.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-flare.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-flare.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/cursors/action-flare.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/cursors/action-flare.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-flare.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-flare.txt =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/cursors/action-flare.txt (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/cursors/action-flare.txt (revision 25691) @@ -0,0 +1 @@ +1 1 Property changes on: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-flare.txt ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_diffuse.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_diffuse.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_diffuse.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_diffuse.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_diffuse.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare-highlight.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = image/png Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare-highlight.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare-highlight.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare-highlight.png (revision 25691) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/minimap-flare-highlight.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +image/png \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/manual/intro.txt =================================================================== --- ps/trunk/binaries/data/mods/public/gui/manual/intro.txt (revision 25690) +++ ps/trunk/binaries/data/mods/public/gui/manual/intro.txt (revision 25691) @@ -1,154 +1,155 @@ [font="sans-bold-18"]0 A.D. in-game manual[font="sans-14"] Thank you for installing 0 A.D.! This page will give a brief overview of the features available in this incomplete, under-development, alpha version of the game. [font="sans-bold-16"]Graphics settings[font="sans-14"] You can switch between fullscreen and windowed mode by pressing Alt + Enter. In windowed mode, you can resize the window. If the game runs too slowly, you can change some settings in the options window: try disabling the “HQ Water Effects” and “Shadows” options. [font="sans-bold-16"]Playing the game[font="sans-14"] The controls and gameplay should be familiar to players of traditional real-time strategy (RTS) games. There are currently a lot of missing features and poorly-balanced stats – you will probably have to wait until a beta release for it to work well. Basic controls (by default): • Left-click to select units • Left-click-and-drag to select groups of units • Right-click to order units to the target • Arrow keys or WASD keys to move the camera • Ctrl + arrow keys, or shift + mouse wheel, to rotate the camera • Mouse wheel, or “hotkey.camera.zoom.in” and “hotkey.camera.zoom.out” keys, to zoom [font="sans-bold-16"]Modes[font="sans-14"] The main menu gives access to two game modes: • [font="sans-bold-14"]Single-player[font="sans-14"] – Play a sandbox game or against one or more computer players (AI). The default AI (Petra) is under development and may not always be up to date on the new features, but you can play the game with or against it nonetheless. • [font="sans-bold-14"]Multiplayer[font="sans-14"] – Play against human opponents over the internet. To set up a multiplayer game, one player must select the “Host game” option. The game uses UDP port 20595, so the host must configure their NAT/firewall/etc. to allow this. Other players can then select “Join game” and enter the host's IP address. [font="sans-bold-16"]Game setup[font="sans-14"] In a multiplayer game, only the host can alter the game setup options. First, select what type of map you want to play: • [font="sans-bold-14"]Random map[font="sans-14"] – A map created automatically from a script • [font="sans-bold-14"]Scenario map[font="sans-14"] – A map created by map designers and with fixed civilizations • [font="sans-bold-14"]Skirmish map[font="sans-14"] – A map created by map designers but where the civilizations can be chosen Then the maps can be further filtered. The default maps are generally playable maps. Naval maps are maps where not all opponents can be reached over land and demo maps are maps used for testing purposes (generally not useful to play on). The “All Maps” filter shows all maps together in one list. Finally change the settings. For random maps this includes the number of players, the size of a map, etc. For scenarios you can only select who controls which player (decides where you start on the map etc.). The options are either a human player, an AI or no player at all (the opponent will just be idle). When you are ready to start, click the “Start game” button. [font="sans-bold-16"]Hotkeys[font="sans-14"] You may change hotkeys in [font="sans-bold-14"]Options > Hotkeys[font="sans-14"] to suit your liking. [font="sans-bold-14"]Program-wide[font="sans-14"] hotkey.exit – Immediately close the game, without asking for confirmation hotkey.togglefullscreen – Toggle between fullscreen and windowed hotkey.console.toggle – Toggle console hotkey.fps.toggle – Toggle frame counter (FPS) hotkey.profile.toggle – Toggle real-time profiler (cycles through the displays of information) hotkey.profile.save – Save current profiler data to “logs/profile.txt” hotkey.screenshot – Take screenshot (in .png format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there) hotkey.bigscreenshot – Take huge screenshot (6400×4800 pixels, in .bmp format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there) hotkey.tab.next – Switch to the next tab hotkey.tab.prev – Switch to the previous tab [font="sans-bold-14"]In Game[font="sans-14"] Double Left Click \[on unit] – Select all of your units of the same kind on the screen (even if they're different ranks) Triple Left Click \[on unit] – Select all of your units of the same kind and the same rank on the screen Alt + Double Left Click \[on unit] – Select all your units of the same kind on the entire map (even if they have different ranks) Alt + Triple Left Click \[on unit] – Select all your units of the same kind and rank on the entire map hotkey.quicksave – Quicksave hotkey.quickload – Quickload hotkey.session.gui.menu.toggle – Toggle menu hotkey.timeelapsedcounter.toggle – Toggle time elapsed counter hotkey.cancel – Close all dialogs (chat, menu) hotkey.confirm – Open chat or send message hotkey.teamchat – Send team chat hotkey.privatechat – Chat with the previously selected private chat partner hotkey.pause – Pause or resume the game hotkey.session.kill – Delete currently selected unit(s)/structure(s), ask for confirmation • With hotkey.session.noconfirmation – Immediately delete currently selected unit(s)/structure(s), without asking for confirmation hotkey.selection.add – Modifier – add to selection (works with clicking and hotkeys, e.g. the idle hotkeys) hotkey.selection.remove – Modifier – remove from selection (works with clicking and hotkeys, e.g. the idle hotkeys) hotkey.selection.offscreen – Modifier – add all units, including offscreen units, to selection. hotkey.selection.cancel – Unselect all units, cancel building placement. hotkey.selection.idlebuilder – Select idle builder hotkey.selection.idlewarrior – Select idle fighter hotkey.selection.idleworker – Select idle worker (including citizen-soldiers) hotkey.selection.idleunit – Select idle unit hotkey.session.stop – Stop (halt) the currently selected units hotkey.session.backtowork – The unit will go back to work hotkey.session.unload – Unload the garrisoned units of the selected structure(s) hotkey.selection.group.save.1 – Create control group 1 (by default, respectively 2,3, ...) from the selected unit(s)/structure(s) hotkey.selection.group.select.1 – Select the unit(s)/structure(s) in control group 1 hotkey.selection.group.add.1 – Add control group 1 (to 0) to the selected unit(s)/structure(s) hotkey.camera.jump.set.1 – Mark the current camera position, for jumping back to later (by default, there are 4 possible positions) hotkey.camera.jump.1 – Move the camera to a marked position. Jump back to the last location if the camera is already over the marked position hotkey.session.queueunit.1, hotkey.session.queueunit.2, hotkey.session.queueunit.3, hotkey.session.queueunit.4, hotkey.session.queueunit.5, hotkey.session.queueunit.6, hotkey.session.queueunit.7 – With training structure selected. Add the 1st, 2nd, … unit shown to the training queue for all the selected structures hotkey.session.highlightguarded with unit(s) selected – Highlight the unit(s)/structure(s) guarded by the selection hotkey.session.highlightguarding with unit(s)/structure(s) selected – Highlight the unit(s) guarding the selection highlightguarding.showstatusbars – See all status bars (which would also show the building progress) hotkey.summary – Toggle summary window hotkey.lobby – Show the multiplayer lobby in a dialog window hotkey.session.gui.diplomacy.toggle – Toggle in-game diplomacy panel hotkey.session.gui.barter.toggle – Toggle in-game barter and trade panel hotkey.session.gui.objectives.toggle – Toggle in-game objectives panel hotkey.session.gui.tutorial.toggle – Toggle in-game tutorial panel hotkey.structree – Toggle structure tree panel hotkey.civinfo – Toggle civilization info panel [font="sans-bold-14"]Modify mouse action[font="sans-14"] hotkey.session.garrison + Right Click on structure – Garrison hotkey.session.repair + Right Click on structure – Repair hotkey.session.patrol + Right Click – Patrol hotkey.session.queue + Right Click – Queue the move/build/gather/etc. order hotkey.session.orderone + Right Click – Order one unit from the current selection to move/build/gather/etc. and unselect it. Used to quickly dispatch units with specific tasks hotkey.session.batchtrain + Left Click when training units – Add units in batches (the batch size is 5 by default and can be changed in the options) hotkey.selection.add + Left Click or Left Drag over unit on map – Add unit to selection hotkey.selection.remove + Left Click or Left Drag over unit on map – Remove unit from selection hotkey.selection.militaryonly + Left Drag over units on map – Only select military units hotkey.selection.nonmilitaryonly + Left Drag over units on map – Only select non-military units hotkey.selection.idleonly + Left Drag over units on map – Only select idle units hotkey.selection.woundedonly + Left Drag over units on map – Only select wounded units Right Click with a structure(s) selected – Set a rally point for units created/ungarrisoned from that structure hotkey.session.garrison + Right Click with unit(s) selected – Garrison (If the cursor is over an own or allied structure) hotkey.session.attack + Right Click with unit(s) selected – Attack (instead of capture or gather) hotkey.session.attackmove + Right Click with unit(s) selected – Attack move (by default all enemy units and structures along the way are targeted) hotkey.session.attackmoveUnit + Right Click with unit(s) selected – Attack move, only units along the way are targeted hotkey.session.snaptoedges + Mouse Move near structures – Align the new structure with an existing nearby structure + hotkey.session.flare + Right Click – Send a flare to your allies [font="sans-bold-14"]Overlays[font="sans-14"] hotkey.session.gui.toggle – Toggle the GUI hotkey.session.devcommands.toggle – Toggle developer overlay (with developer options) hotkey.wireframe – Toggle wireframe mode (press once to get wireframes overlaid over the textured models, twice to get just the wireframes colored by the textures, thrice to get back to normal textured mode) hotkey.silhouettes – Toggle unit silhouettes (might give a small performance boost) hotkey.session.diplomacycolors – Toggle diplomacy colors hotkey.session.toggleattackrange – Toggle attack range visualizations of selected units and structures hotkey.session.toggleaurasrange – Toggle aura range visualizations of selected units and structures hotkey.session.togglehealrange – Toggle heal range visualizations of selected units [font="sans-bold-14"]Camera manipulation[font="sans-14"] hotkey.camera.up – Pan screen up hotkey.camera.down – Pan screen down hotkey.camera.left – Pan screen left hotkey.camera.right – Pan screen right hotkey.camera.rotate.up – Rotate camera to look upward hotkey.camera.rotate.down – Rotate camera to look downward hotkey.camera.rotate.cw – Rotate camera clockwise around terrain hotkey.camera.rotate.ccw – Rotate camera counter-clockwise around terrain hotkey.camera.rotate.wheel.cw – Rotate camera clockwise around terrain hotkey.camera.rotate.wheel.ccw – Rotate camera counter-clockwise around terrain hotkey.camera.follow – Follow the selected unit (move the camera to stop following the unit) hotkey.camera.reset – Reset camera zoom and orientation hotkey.camera.zoom.in, hotkey.camera.zoom.wheel.in – Zoom in (keep pressed for continuous zoom) hotkey.camera.zoom.out, hotkey.camera.zoom.wheel.out – Zoom out (keep pressed for continuous zoom) hotkey.camera.pan – Keep pressed and move the mouse to pan [font="sans-bold-14"]During Structure Placement[font="sans-14"] hotkey.session.rotate.ccw – Rotate structure 15 degrees counter-clockwise hotkey.session.rotate.cw – Rotate structure 15 degrees clockwise Left Drag – Rotate structure using mouse (foundation will be placed on mouse release) [font="sans-bold-14"]When loading a saved game[font="sans-14"] hotkey.cancel – Cancel hotkey.session.savedgames.delete – Delete the selected saved game, ask for confirmation • With hotkey.session.savedgames.noconfirmation – Don't ask for confirmation Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 25690) +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 25691) @@ -1,558 +1,595 @@ /** * All tutorial messages received so far. */ var g_TutorialMessages = []; /** * GUI tags applied to the most recent tutorial message. */ var g_TutorialNewMessageTags = { "color": "255 226 149" }; /** + * The number of seconds we observe for rate limiting flares. + */ +var g_FlareRateLimitScope = 10; + +/** + * The maximum allowed number of flares within g_FlareRateLimitScope seconds. + * This should be a bit larger than the number of flares that can be sent in theory by using the GUI. + */ +var g_FlareRateLimitMaximumFlares = 16; + +/** + * Contains the arrival timestamps the flares of the last g_FlareRateLimitScope seconds. + */ +var g_FlareRateLimitLastTimes = []; + +/** * These handlers are called everytime a client joins or disconnects. */ var g_PlayerAssignmentsChangeHandlers = new Set(); /** * These handlers are called when the ceasefire time has run out. */ var g_CeasefireEndedHandlers = new Set(); /** * These handlers are fired when the match is networked and * the current client established the connection, authenticated, * finished the loading screen, starts or finished synchronizing after a rejoin. * The messages are constructed in NetClient.cpp. */ var g_NetworkStatusChangeHandlers = new Set(); /** * These handlers are triggered whenever a client finishes the loading screen. */ var g_ClientsLoadingHandlers = new Set(); /** * These handlers are fired if the server informed the players that the networked game is out of sync. */ var g_NetworkOutOfSyncHandlers = new Set(); /** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { "netstatus": msg => { handleNetStatusMessage(msg); }, "netwarn": msg => { addNetworkWarning(msg); }, "out-of-sync": msg => { for (let handler of g_NetworkOutOfSyncHandlers) handler(msg); }, "players": msg => { handlePlayerAssignmentsMessage(msg); }, "paused": msg => { g_PauseControl.setClientPauseState(msg.guid, msg.pause); }, "clients-loading": msg => { for (let handler of g_ClientsLoadingHandlers) handler(msg.guids); }, "rejoined": msg => { addChatMessage({ "type": "rejoined", "guid": msg.guid }); }, "kicked": msg => { addChatMessage({ "type": "kicked", "username": msg.username, "banned": msg.banned }); }, "chat": msg => { addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text }); }, "gamesetup": msg => {}, // Needed for autostart "start": msg => {} }; var g_PlayerStateMessages = { "won": translate("You have won!"), "defeated": translate("You have been defeated!") }; /** * Defines how the GUI reacts to notifications that are sent by the simulation. * Don't open new pages (message boxes) here! Otherwise further notifications * handled in the same turn can't access the GUI objects anymore. */ var g_NotificationsTypes = { "aichat": function(notification, player) { let message = { "type": "message", "text": notification.message, "guid": findGuidForPlayerID(player) || -1, "player": player, "translate": true }; if (notification.translateParameters) { message.translateParameters = notification.translateParameters; message.parameters = notification.parameters; colorizePlayernameParameters(notification.parameters); } addChatMessage(message); }, "defeat": function(notification, player) { playersFinished(notification.allies, notification.message, false); }, "won": function(notification, player) { playersFinished(notification.allies, notification.message, true); }, "diplomacy": function(notification, player) { updatePlayerData(); g_DiplomacyColors.onDiplomacyChange(); addChatMessage({ "type": "diplomacy", "sourcePlayer": player, "targetPlayer": notification.targetPlayer, "status": notification.status }); }, "ceasefire-ended": function(notification, player) { updatePlayerData(); for (let handler of g_CeasefireEndedHandlers) handler(); }, "tutorial": function(notification, player) { updateTutorial(notification); }, "tribute": function(notification, player) { addChatMessage({ "type": "tribute", "sourcePlayer": notification.donator, "targetPlayer": player, "amounts": notification.amounts }); }, "barter": function(notification, player) { addChatMessage({ "type": "barter", "player": player, "amountGiven": notification.amountGiven, "amountGained": notification.amountGained, "resourceGiven": notification.resourceGiven, "resourceGained": notification.resourceGained }); }, "spy-response": function(notification, player) { g_DiplomacyDialog.onSpyResponse(notification, player); if (notification.entity && g_ViewedPlayer == player) { g_DiplomacyDialog.close(); setCameraFollow(notification.entity); } }, "attack": function(notification, player) { if (player != g_ViewedPlayer) return; // Focus camera on attacks if (g_FollowPlayer) { setCameraFollow(notification.target); g_Selection.reset(); if (notification.target) g_Selection.addList([notification.target]); } if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.attack") !== "true") return; addChatMessage({ "type": "attack", "player": player, "attacker": notification.attacker, "target": notification.target, "position": notification.position, "targetIsDomesticAnimal": notification.targetIsDomesticAnimal }); }, "phase": function(notification, player) { addChatMessage({ "type": "phase", "player": player, "phaseName": notification.phaseName, "phaseState": notification.phaseState }); }, "dialog": function(notification, player) { if (player == Engine.GetPlayerID()) openDialog(notification.dialogName, notification.data, player); }, "playercommand": function(notification, player) { // For observers, focus the camera on units commanded by the selected player if (!g_FollowPlayer || player != g_ViewedPlayer) return; let cmd = notification.cmd; // Ignore rallypoint commands of trained animals let entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]); if (g_ViewedPlayer != 0 && entState && entState.identity && entState.identity.classes && entState.identity.classes.indexOf("Animal") != -1) return; // Focus the structure to build. if (cmd.type == "repair") { let targetState = GetEntityState(cmd.target); if (targetState) Engine.CameraMoveTo(targetState.position.x, targetState.position.z); } else if (cmd.type == "delete-entities" && notification.position) Engine.CameraMoveTo(notification.position.x, notification.position.y); // Focus commanded entities, but don't lose previous focus when training units else if (cmd.type != "train" && cmd.type != "research" && entState) setCameraFollow(cmd.entities[0]); if (["walk", "attack-walk", "patrol"].indexOf(cmd.type) != -1) DrawTargetMarker(cmd); // Select units affected by that command let selection = []; if (cmd.entities) selection = cmd.entities; if (cmd.target) selection.push(cmd.target); // Allow gaia in selection when gathering g_Selection.reset(); g_Selection.addList(selection, false, cmd.type == "gather"); }, "play-tracks": function(notification, player) { if (notification.lock) { global.music.storeTracks(notification.tracks.map(track => ({ "Type": "custom", "File": track }))); global.music.setState(global.music.states.CUSTOM); } global.music.setLocked(notification.lock); + }, + "map-flare": function(notification, player) + { + // Don't display for the player that did the flare because they will see it immediately + if (player != Engine.GetPlayerID() && g_Players[player].isMutualAlly[Engine.GetPlayerID()]) + { + let now = Date.now(); + if (g_FlareRateLimitLastTimes.length) + { + g_FlareRateLimitLastTimes = g_FlareRateLimitLastTimes.filter(t => now - t < g_FlareRateLimitScope * 1000); + if (g_FlareRateLimitLastTimes.length >= g_FlareRateLimitMaximumFlares) + { + warn("Received too many flares. Dropping a flare request by '" + g_Players[player].name + "'."); + return; + } + } + g_FlareRateLimitLastTimes.push(now); + + displayFlare(notification.target, player); + Engine.PlayUISound(g_FlareSound, false); + } } }; function registerPlayerAssignmentsChangeHandler(handler) { g_PlayerAssignmentsChangeHandlers.add(handler); } function registerCeasefireEndedHandler(handler) { g_CeasefireEndedHandlers.add(handler); } function registerNetworkOutOfSyncHandler(handler) { g_NetworkOutOfSyncHandlers.add(handler); } function registerNetworkStatusChangeHandler(handler) { g_NetworkStatusChangeHandlers.add(handler); } function registerClientsLoadingHandler(handler) { g_ClientsLoadingHandlers.add(handler); } function findGuidForPlayerID(playerID) { return Object.keys(g_PlayerAssignments).find(guid => g_PlayerAssignments[guid].player == playerID); } /** * Processes all pending notifications sent from the GUIInterface simulation component. */ function handleNotifications() { for (let notification of Engine.GuiInterfaceCall("GetNotifications")) { if (!notification.players || !notification.type || !g_NotificationsTypes[notification.type]) { error("Invalid GUI notification: " + uneval(notification)); continue; } for (let player of notification.players) g_NotificationsTypes[notification.type](notification, player); } } function toggleTutorial() { let tutorialPanel = Engine.GetGUIObjectByName("tutorialPanel"); tutorialPanel.hidden = !tutorialPanel.hidden || !Engine.GetGUIObjectByName("tutorialText").caption; } /** * Updates the tutorial panel when a new goal. */ function updateTutorial(notification) { // Show the tutorial panel if not yet done Engine.GetGUIObjectByName("tutorialPanel").hidden = false; if (notification.warning) { Engine.GetGUIObjectByName("tutorialWarning").caption = coloredText(translate(notification.warning), "orange"); return; } let notificationText = notification.instructions.reduce((instructions, item) => instructions + (typeof item == "string" ? translate(item) : colorizeHotkey(translate(item.text), item.hotkey)), ""); Engine.GetGUIObjectByName("tutorialText").caption = g_TutorialMessages.concat(setStringTags(notificationText, g_TutorialNewMessageTags)).join("\n"); g_TutorialMessages.push(notificationText); if (notification.readyButton) { Engine.GetGUIObjectByName("tutorialReady").hidden = false; if (notification.leave) { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click to quit this tutorial."); Engine.GetGUIObjectByName("tutorialReady").caption = translate("Quit"); Engine.GetGUIObjectByName("tutorialReady").onPress = endGame; } else Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click when ready."); } else { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Follow the instructions."); Engine.GetGUIObjectByName("tutorialReady").hidden = true; } } /** * Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. * Saves the received object to mainlog.html. */ function handleNetMessages() { while (true) { let msg = Engine.PollNetworkClient(); if (!msg) return; log("Net message: " + uneval(msg)); if (g_NetMessageTypes[msg.type]) g_NetMessageTypes[msg.type](msg); else error("Unrecognised net message type '" + msg.type + "'"); } } function handleNetStatusMessage(message) { if (g_Disconnected) return; g_IsNetworkedActive = message.status == "active"; if (message.status == "disconnected") { g_Disconnected = true; updateCinemaPath(); closeOpenDialogs(); } for (let handler of g_NetworkStatusChangeHandlers) handler(message); } function handlePlayerAssignmentsMessage(message) { for (let guid in g_PlayerAssignments) if (!message.newAssignments[guid]) onClientLeave(guid); let joins = Object.keys(message.newAssignments).filter(guid => !g_PlayerAssignments[guid]); g_PlayerAssignments = message.newAssignments; joins.forEach(guid => { onClientJoin(guid); }); for (let handler of g_PlayerAssignmentsChangeHandlers) handler(); // TODO: use subscription instead updateGUIObjects(); } function onClientJoin(guid) { let playerID = g_PlayerAssignments[guid].player; if (g_Players[playerID]) { g_Players[playerID].guid = guid; g_Players[playerID].name = g_PlayerAssignments[guid].name; g_Players[playerID].offline = false; } addChatMessage({ "type": "connect", "guid": guid }); } function onClientLeave(guid) { g_PauseControl.setClientPauseState(guid, false); for (let id in g_Players) if (g_Players[id].guid == guid) g_Players[id].offline = true; addChatMessage({ "type": "disconnect", "guid": guid }); } function addChatMessage(msg) { g_Chat.ChatMessageHandler.handleMessage(msg); } function clearChatMessages() { g_Chat.ChatOverlay.clearChatMessages(); } /** * This function is used for AIs, whose names don't exist in g_PlayerAssignments. */ function colorizePlayernameByID(playerID) { let username = g_Players[playerID] && escapeText(g_Players[playerID].name); return colorizePlayernameHelper(username, playerID); } function colorizePlayernameByGUID(guid) { let username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : ""; let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1; return colorizePlayernameHelper(username, playerID); } function colorizePlayernameHelper(username, playerID) { let playerColor = playerID > -1 ? g_DiplomacyColors.getPlayerColor(playerID) : "white"; return coloredText(username || translate("Unknown Player"), playerColor); } /** * Insert the colorized playername to chat messages sent by the AI and time notifications. */ function colorizePlayernameParameters(parameters) { for (let param in parameters) if (param.startsWith("_player_")) parameters[param] = colorizePlayernameByID(parameters[param]); } /** * Custom dialog response handling, usable by trigger maps. */ function sendDialogAnswer(guiObject, dialogName) { Engine.GetGUIObjectByName(dialogName + "-dialog").hidden = true; Engine.PostNetworkCommand({ "type": "dialog-answer", "dialog": dialogName, "answer": guiObject.name.split("-").pop(), }); resumeGame(); } /** * Custom dialog opening, usable by trigger maps. */ function openDialog(dialogName, data, player) { let dialog = Engine.GetGUIObjectByName(dialogName + "-dialog"); if (!dialog) { warn("messages.js: Unknown dialog with name " + dialogName); return; } dialog.hidden = false; for (let objName in data) { let obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName); if (!obj) { warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog."); continue; } for (let key in data[objName]) { let n = data[objName][key]; if (typeof n == "object" && n.message) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); obj[key] = sprintf(message, parameters); } else obj[key] = n; } } g_PauseControl.implicitPause(); } Index: ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMap.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMap.xml (revision 25690) +++ ps/trunk/binaries/data/mods/public/gui/session/minimap/MiniMap.xml (revision 25691) @@ -1,38 +1,60 @@ + + + - + Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 25690) +++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 25691) @@ -1,1872 +1,1889 @@ /** * Specifies which template should indicate the target location of a player command, * given a command type. */ var g_TargetMarker = { - "move": "special/target_marker" + "move": "special/target_marker", + "map_flare": "special/flare_target_marker" }; /** + * Sound we play when displaying a flare. + */ +var g_FlareSound = "audio/interface/alarm/alarmally_1.ogg"; + +/** * Which enemy entity types will be attacked on sight when patroling. */ var g_PatrolTargets = ["Unit"]; const g_DisabledTags = { "color": "255 140 0" }; /** * List of different actions units can execute, * this is mostly used to determine which actions can be executed * * "execute" is meant to send the command to the engine * * The next functions will always return false * in case you have to continue to seek * (i.e. look at the next entity for getActionInfo, the next * possible action for the actionCheck ...) * They will return an object when the searching is finished * * "getActionInfo" is used to determine if the action is possible, * and also give visual feedback to the user (tooltips, cursors, ...) * * "preSelectedActionCheck" is used to select actions when the gui buttons * were used to set them, but still require a target (like the guard button) * * "hotkeyActionCheck" is used to check the possibility of actions when * a hotkey is pressed * * "actionCheck" is used to check the possibilty of actions without specific * command. For that, the specificness variable is used * * "specificness" is used to determine how specific an action is, * The lower the number, the more specific an action is, and the bigger * the chance of selecting that action when multiple actions are possible */ var g_UnitActions = { "move": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getDefault() }); DrawTargetMarker(target); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.unitAI) return false; return { "possible": true }; }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.move") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("move", target, selection); return actionInfo.possible && { "type": "move", "firstAbleEntity": actionInfo.entity }; }, "specificness": 12, }, "attack-move": { "execute": function(target, action, selection, queued, pushFront) { let targetClasses; if (Engine.HotkeyIsPressed("session.attackmoveUnit")) targetClasses = { "attack": ["Unit"] }; else targetClasses = { "attack": ["Unit", "Structure"] }; Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); DrawTargetMarker(target); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.unitAI) return false; return { "possible": true }; }, "hotkeyActionCheck": function(target, selection) { return isAttackMovePressed() && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("attack-move", target, selection); return actionInfo.possible && { "type": "attack-move", "cursor": "action-attack-move", "firstAbleEntity": actionInfo.entity }; }, "specificness": 30, }, "capture": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "allowCapture": true, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.attack || !targetState || !targetState.capturePoints) return false; return { "possible": Engine.GuiInterfaceCall("CanAttack", { "entity": entState.id, "target": targetState.id, "types": ["Capture"] }) }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("capture", target, selection); return actionInfo.possible && { "type": "capture", "cursor": "action-capture", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 9, }, "attack": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "allowCapture": false, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.attack || !targetState || !targetState.hitpoints) return false; return { "possible": Engine.GuiInterfaceCall("CanAttack", { "entity": entState.id, "target": targetState.id, "types": ["!Capture"] }) }; }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.attack") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("attack", target, selection); return actionInfo.possible && { "type": "attack", "cursor": "action-attack", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 10, }, "patrol": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "patrol", "entities": selection, "x": target.x, "z": target.z, "target": action.target, "targetClasses": { "attack": g_PatrolTargets }, "queued": queued, "allowCapture": false, "formation": g_AutoFormation.getDefault() }); DrawTargetMarker(target); Engine.GuiInterfaceCall("PlaySound", { "name": "order_patrol", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.unitAI || !entState.unitAI.canPatrol) return false; return { "possible": true }; }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.patrol") && this.actionCheck(target, selection); }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_PATROL && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("patrol", target, selection); return actionInfo.possible && { "type": "patrol", "cursor": "action-patrol", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 37, }, "heal": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "heal", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.heal || !targetState || !hasClass(targetState, "Unit") || !targetState.needsHeal || !playerCheck(entState, targetState, ["Player", "Ally"]) || entState.id == targetState.id) // Healers can't heal themselves. return false; let unhealableClasses = entState.heal.unhealableClasses; if (MatchesClassList(targetState.identity.classes, unhealableClasses)) return false; let healableClasses = entState.heal.healableClasses; if (!MatchesClassList(targetState.identity.classes, healableClasses)) return false; return { "possible": true }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("heal", target, selection); return actionInfo.possible && { "type": "heal", "cursor": "action-heal", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 7, }, // "Fake" action to check if an entity can be ordered to "construct" // which is handled differently from repair as the target does not exist. "construct": { "preSelectedActionCheck": function(target, selection) { let state = GetEntityState(selection[0]); if (state && state.builder && target && target.constructor && target.constructor.name == "PlacementSupport") return { "type": "construct" }; return false; }, "specificness": 0, }, "repair": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": action.foundation ? "order_build" : "order_repair", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.builder || !targetState || !targetState.needsRepair && !targetState.foundation || !playerCheck(entState, targetState, ["Player", "Ally"])) return false; return { "possible": true, "foundation": targetState.foundation }; }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_REPAIR && (this.actionCheck(target, selection) || { "type": "none", "cursor": "action-repair-disabled", "target": null }); }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.repair") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("repair", target, selection); return actionInfo.possible && { "type": "repair", "cursor": "action-repair", "target": target, "foundation": actionInfo.foundation, "firstAbleEntity": actionInfo.entity }; }, "specificness": 11, }, "gather": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "gather", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.resourceGatherRates || !targetState || !targetState.resourceSupply) return false; let resource; if (entState.resourceGatherRates[targetState.resourceSupply.type.generic + "." + targetState.resourceSupply.type.specific]) resource = targetState.resourceSupply.type.specific; else if (entState.resourceGatherRates[targetState.resourceSupply.type.generic]) resource = targetState.resourceSupply.type.generic; if (!resource) return false; return { "possible": true, "cursor": "action-gather-" + resource }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("gather", target, selection); return actionInfo.possible && { "type": "gather", "cursor": actionInfo.cursor, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 1, }, "returnresource": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "returnresource", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || !targetState.resourceDropsite) return false; let playerState = GetSimState().players[entState.player]; if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared) { if (!playerCheck(entState, targetState, ["Player", "MutualAlly"])) return false; } else if (!playerCheck(entState, targetState, ["Player"])) return false; if (!entState.resourceCarrying || !entState.resourceCarrying.length) return false; let carriedType = entState.resourceCarrying[0].type; if (targetState.resourceDropsite.types.indexOf(carriedType) == -1) return false; return { "possible": true, "cursor": "action-return-" + carriedType }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("returnresource", target, selection); return actionInfo.possible && { "type": "returnresource", "cursor": actionInfo.cursor, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 2, }, "cancel-setup-trade-route": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "cancel-setup-trade-route", "entities": selection, "target": action.target, "queued": queued }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || targetState.foundation || !entState.trader || !targetState.market || playerCheck(entState, targetState, ["Enemy"]) || !(targetState.market.land && hasClass(entState, "Organic") || targetState.market.naval && hasClass(entState, "Ship"))) return false; let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", { "trader": entState.id, "target": targetState.id }); if (!tradingDetails || !tradingDetails.type) return false; if (tradingDetails.type == "is first" && !tradingDetails.hasBothMarkets) return { "possible": true, "tooltip": translate("This is the origin trade market.\nRight-click to cancel trade route.") }; return false; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("cancel-setup-trade-route", target, selection); return actionInfo.possible && { "type": "cancel-setup-trade-route", "cursor": "action-cancel-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 2, }, "setup-trade-route": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({ "type": "setup-trade-route", "entities": selection, "target": action.target, "source": null, "route": null, "queued": queued, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || targetState.foundation || !entState.trader || !targetState.market || playerCheck(entState, targetState, ["Enemy"]) || !(targetState.market.land && hasClass(entState, "Organic") || targetState.market.naval && hasClass(entState, "Ship"))) return false; let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", { "trader": entState.id, "target": targetState.id }); if (!tradingDetails) return false; let tooltip; switch (tradingDetails.type) { case "is first": tooltip = translate("Origin trade market.") + "\n"; if (tradingDetails.hasBothMarkets) tooltip += sprintf(translate("Gain: %(gain)s"), { "gain": getTradingTooltip(tradingDetails.gain) }); else return false; break; case "is second": tooltip = translate("Destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), { "gain": getTradingTooltip(tradingDetails.gain) }); break; case "set first": tooltip = translate("Right-click to set as origin trade market"); break; case "set second": if (tradingDetails.gain.traderGain == 0) return { "possible": true, "tooltip": setStringTags(translate("This market is too close to the origin market."), g_DisabledTags), "disabled": true }; tooltip = translate("Right-click to set as destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), { "gain": getTradingTooltip(tradingDetails.gain) }); break; } return { "possible": true, "tooltip": tooltip }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("setup-trade-route", target, selection); if (actionInfo.disabled) return { "type": "none", "cursor": "action-setup-trade-route-disabled", "target": null, "tooltip": actionInfo.tooltip }; return actionInfo.possible && { "type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 0, }, "occupy-turret": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "occupy-turret", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.turretable || !targetState || !targetState.turretHolder || !playerCheck(entState, targetState, ["Player", "MutualAlly"])) return false; if (!targetState.turretHolder.turretPoints.find(point => !point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses))) return false; let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null); let tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), { "occupied": occupiedTurrets.length, "capacity": targetState.turretHolder.turretPoints.length }); if (occupiedTurrets.length == targetState.turretHolder.turretPoints.length) tooltip = coloredText(tooltip, "orange"); return { "possible": true, "tooltip": tooltip }; }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_OCCUPY_TURRET && (this.actionCheck(target, selection) || { "type": "none", "cursor": "action-occupy-turret-disabled", "target": null }); }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.occupyturret") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("occupy-turret", target, selection); return actionInfo.possible && { "type": "occupy-turret", "cursor": "action-occupy-turret", "tooltip": actionInfo.tooltip, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 21, }, "garrison": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "garrison", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.garrisonable || !targetState || !targetState.garrisonHolder || !playerCheck(entState, targetState, ["Player", "MutualAlly"])) return false; let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { "garrisoned": targetState.garrisonHolder.occupiedSlots, "capacity": targetState.garrisonHolder.capacity }); let extraCount = entState.garrisonable.size; if (entState.garrisonHolder) extraCount += entState.garrisonHolder.occupiedSlots; if (targetState.garrisonHolder.occupiedSlots + extraCount > targetState.garrisonHolder.capacity) tooltip = coloredText(tooltip, "orange"); if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses)) return false; return { "possible": true, "tooltip": tooltip }; }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_GARRISON && (this.actionCheck(target, selection) || { "type": "none", "cursor": "action-garrison-disabled", "target": null }); }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.garrison") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("garrison", target, selection); return actionInfo.possible && { "type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 20, }, "guard": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "guard", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || !targetState.guard || entState.id == targetState.id || !playerCheck(entState, targetState, ["Player", "Ally"]) || !entState.unitAI || !entState.unitAI.canGuard) return false; return { "possible": true }; }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_GUARD && (this.actionCheck(target, selection) || { "type": "none", "cursor": "action-guard-disabled", "target": null }); }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.guard") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("guard", target, selection); return actionInfo.possible && { "type": "guard", "cursor": "action-guard", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 40, }, "collect-treasure": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({ "type": "collect-treasure", "entities": selection, "target": action.target, "queued": queued, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_collect_treasure", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.treasureCollector || !targetState || !targetState.treasure) return false; return { "possible": true, "cursor": "action-collect-treasure" }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("collect-treasure", target, selection); return actionInfo.possible && { "type": "collect-treasure", "cursor": actionInfo.cursor, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 1, }, "remove-guard": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "remove-guard", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.unitAI || !entState.unitAI.isGuarding) return false; return { "possible": true }; }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.guard") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("remove-guard", target, selection); return actionInfo.possible && { "type": "remove-guard", "cursor": "action-remove-guard", "firstAbleEntity": actionInfo.entity }; }, "specificness": 41, }, "set-rallypoint": { "execute": function(target, action, selection, queued, pushFront) { // if there is a position set in the action then use this so that when setting a // rally point on an entity it is centered on that entity if (action.position) target = action.position; Engine.PostNetworkCommand({ "type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z, "data": action.data, "queued": queued }); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": target.x, "z": target.z, "queued": queued }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.rallyPoint) return false; // Don't allow the rally point to be set on any of the currently selected entities (used for unset) // except if the autorallypoint hotkey is pressed and the target can produce entities. if (targetState && (!Engine.HotkeyIsPressed("session.autorallypoint") || !targetState.production || !targetState.production.entities.length)) for (const ent of g_Selection.toList()) if (targetState.id == ent) return false; let tooltip; let disabled = false; // default to walking there (or attack-walking if hotkey pressed) let data = { "command": "walk" }; let cursor = ""; if (isAttackMovePressed()) { let targetClasses; if (Engine.HotkeyIsPressed("session.attackmoveUnit")) targetClasses = { "attack": ["Unit"] }; else targetClasses = { "attack": ["Unit", "Structure"] }; data.command = "attack-walk"; data.targetClasses = targetClasses; cursor = "action-attack-move"; } if (Engine.HotkeyIsPressed("session.repair") && targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Player", "Ally"])) { data.command = "repair"; data.target = targetState.id; cursor = "action-repair"; } else if (targetState && targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"])) { data.command = "garrison"; data.target = targetState.id; cursor = "action-garrison"; tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { "garrisoned": targetState.garrisonHolder.occupiedSlots, "capacity": targetState.garrisonHolder.capacity }); if (targetState.garrisonHolder.occupiedSlots >= targetState.garrisonHolder.capacity) tooltip = coloredText(tooltip, "orange"); } else if (targetState && targetState.turretHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"])) { data.command = "occupy-turret"; data.target = targetState.id; cursor = "action-garrison"; let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null); tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), { "occupied": occupiedTurrets.length, "capacity": targetState.turretHolder.turretPoints.length }); if (occupiedTurrets.length >= targetState.turretHolder.turretPoints.length) tooltip = coloredText(tooltip, "orange"); } else if (targetState && targetState.resourceSupply) { let resourceType = targetState.resourceSupply.type; cursor = "action-gather-" + resourceType.specific; data.command = "gather-near-position"; data.resourceType = resourceType; data.resourceTemplate = targetState.template; if (!targetState.speed) { data.command = "gather"; data.target = targetState.id; } } else if (targetState && targetState.treasure) { cursor = "action-collect-treasure"; data.command = "collect-treasure-near-position"; if (!targetState.speed) { data.command = "collect-treasure"; data.target = targetState.id; } } else if (entState.market && targetState && targetState.market && entState.id != targetState.id && (!entState.market.naval || targetState.market.naval) && !playerCheck(entState, targetState, ["Enemy"])) { // Find a trader (if any) that this structure can train. let trader; if (entState.production && entState.production.entities.length) for (let i = 0; i < entState.production.entities.length; ++i) if ((trader = GetTemplateData(entState.production.entities[i]).trader)) break; let traderData = { "firstMarket": entState.id, "secondMarket": targetState.id, "template": trader }; let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData); if (gain) { data.command = "trade"; data.target = traderData.secondMarket; data.source = traderData.firstMarket; cursor = "action-setup-trade-route"; if (gain.traderGain) tooltip = translate("Right-click to establish a default route for new traders.") + "\n" + sprintf( trader ? translate("Gain: %(gain)s") : translate("Expected gain: %(gain)s"), { "gain": getTradingTooltip(gain) }); else { disabled = true; tooltip = setStringTags(translate("This market is too close to the origin market."), g_DisabledTags); cursor = "action-setup-trade-route-disabled"; } } } else if (targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"])) { data.command = "repair"; data.target = targetState.id; cursor = "action-repair"; } else if (targetState && playerCheck(entState, targetState, ["Enemy"])) { data.target = targetState.id; data.command = "attack"; cursor = "action-attack"; } return { "possible": true, "data": data, "position": targetState && targetState.position, "cursor": cursor, "disabled": disabled, "tooltip": tooltip }; }, "hotkeyActionCheck": function(target, selection) { // Hotkeys are checked in the actionInfo. return this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { // We want commands to units take precedence. if (selection.some(ent => { let entState = GetEntityState(ent); return entState && !!entState.unitAI; })) return false; let actionInfo = getActionInfo("set-rallypoint", target, selection); if (actionInfo.disabled) return { "type": "none", "cursor": actionInfo.cursor, "target": null, "tooltip": actionInfo.tooltip }; return actionInfo.possible && { "type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position, "firstAbleEntity": actionInfo.entity }; }, "specificness": 6, }, "unset-rallypoint": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "unset-rallypoint", "entities": selection }); // Remove displayed rally point Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": [] }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || entState.id != targetState.id || entState.unitAI || !entState.rallyPoint || !entState.rallyPoint.position) return false; return { "possible": true }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("unset-rallypoint", target, selection); return actionInfo.possible && { "type": "unset-rallypoint", "cursor": "action-unset-rally", "firstAbleEntity": actionInfo.entity }; }, "specificness": 11, }, // This is a "fake" action to show a failure cursor // when only uncontrollable entities are selected. "uncontrollable": { "execute": function(target, action, selection, queued) { return true; }, "actionCheck": function(target, selection) { // Only show this action if all entities are marked uncontrollable. let playerState = g_SimState.players[g_ViewedPlayer]; if (playerState && playerState.controlsAll || selection.some(ent => { let entState = GetEntityState(ent); return entState && entState.identity && entState.identity.controllable; })) return false; return { "type": "none", "cursor": "cursor-no", "tooltip": translatePlural("This entity cannot be controlled.", "These entities cannot be controlled.", selection.length) }; }, "specificness": 100, }, "none": { "execute": function(target, action, selection, queued) { return true; }, "specificness": 100, }, }; var g_UnitActionsSortedKeys = Object.keys(g_UnitActions).sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness); /** * Info and actions for the entity commands * Currently displayed in the bottom of the central panel */ var g_EntityCommands = { "unload-all": { "getInfo": function(entStates) { let count = 0; for (let entState of entStates) { if (!entState.garrisonHolder) continue; if (allowedPlayersCheck([entState], ["Player"])) count += entState.garrisonHolder.entities.length; else for (let entity of entState.garrisonHolder.entities) if (allowedPlayersCheck([GetEntityState(entity)], ["Player"])) ++count; } if (!count) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") + translate("Unload All."), "icon": "garrison-out.png", "count": count, "enabled": true }; }, "execute": function() { unloadAll(); }, "allowedPlayers": ["Player", "Ally"] }, "unload-all-turrets": { "getInfo": function(entStates) { let count = 0; for (let entState of entStates) { if (!entState.turretHolder) continue; if (allowedPlayersCheck([entState], ["Player"])) count += entState.turretHolder.turretPoints.filter(turretPoint => turretPoint.entity && turretPoint.ejectable).length; else for (let turretPoint of entState.turretHolder.turretPoints) if (turretPoint.entity && allowedPlayersCheck([GetEntityState(turretPoint.entity)], ["Player"])) ++count; } if (!count) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unloadturrets") + translate("Unload Turrets."), "icon": "garrison-out.png", "count": count, "enabled": true }; }, "execute": function() { unloadAllTurrets(); }, "allowedPlayers": ["Player", "Ally"] }, "delete": { "getInfo": function(entStates) { return entStates.some(entState => !isUndeletable(entState)) ? { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.kill") + translate("Destroy the selected units or structures.") + "\n" + colorizeHotkey( translate("Use %(hotkey)s to avoid the confirmation dialog."), "session.noconfirmation" ), "icon": "kill_small.png", "enabled": true } : { // Get all delete reasons and remove duplications "tooltip": entStates.map(entState => isUndeletable(entState)) .filter((reason, pos, self) => self.indexOf(reason) == pos && reason ).join("\n"), "icon": "kill_small_disabled.png", "enabled": false }; }, "execute": function(entStates) { let entityIDs = entStates.reduce( (ids, entState) => { if (!isUndeletable(entState)) ids.push(entState.id); return ids; }, []); if (!entityIDs.length) return; let deleteSelection = () => Engine.PostNetworkCommand({ "type": "delete-entities", "entities": entityIDs }); if (Engine.HotkeyIsPressed("session.noconfirmation")) deleteSelection(); else (new DeleteSelectionConfirmation(deleteSelection)).display(); }, "allowedPlayers": ["Player"] }, "stop": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.unitAI)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") + translate("Abort the current order."), "icon": "stop.png", "enabled": true }; }, "execute": function(entStates) { if (entStates.length) stopUnits(entStates.map(entState => entState.id)); }, "allowedPlayers": ["Player"] }, "garrison": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.garrisonable || entState.garrisonable.holder != INVALID_ENTITY)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") + translate("Order the selected units to garrison in a structure or unit."), "icon": "garrison.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; }, "allowedPlayers": ["Player"] }, "occupy-turret": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.turretable || entState.turretable.holder != INVALID_ENTITY)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.occupyturret") + translate("Order the selected units to occupy a turret point."), "icon": "occupy-turret.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_OCCUPY_TURRET; }, "allowedPlayers": ["Player"] }, "leave-turret": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.turretable || entState.turretable.holder == INVALID_ENTITY || !entState.turretable.ejectable)) return false; return { "tooltip": translate("Unload"), "icon": "leave-turret.png", "enabled": true }; }, "execute": function(entStates) { leaveTurretPoints(); }, "allowedPlayers": ["Player"] }, "repair": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.builder)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") + translate("Order the selected units to repair a structure, ship, or siege engine."), "icon": "repair.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_REPAIR; }, "allowedPlayers": ["Player"] }, "focus-rally": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.rallyPoint)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") + translate("Focus on Rally Point."), "icon": "focus-rally.png", "enabled": true }; }, "execute": function(entStates) { // TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first let focusTarget; for (let entState of entStates) if (entState.rallyPoint && entState.rallyPoint.position) { focusTarget = entState.rallyPoint.position; break; } if (!focusTarget) for (let entState of entStates) if (entState.position) { focusTarget = entState.position; break; } if (focusTarget) Engine.CameraMoveTo(focusTarget.x, focusTarget.z); }, "allowedPlayers": ["Player", "Observer"] }, "back-to-work": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") + translate("Back to Work"), "icon": "back-to-work.png", "enabled": true }; }, "execute": function() { backToWork(); }, "allowedPlayers": ["Player"] }, "add-guard": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") + translate("Order the selected units to guard a structure or unit."), "icon": "add-guard.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GUARD; }, "allowedPlayers": ["Player"] }, "remove-guard": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding)) return false; return { "tooltip": translate("Remove guard"), "icon": "remove-guard.png", "enabled": true }; }, "execute": function() { removeGuard(); }, "allowedPlayers": ["Player"] }, "select-trading-goods": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.market)) return false; return { "tooltip": translate("Barter & Trade"), "icon": "economics.png", "enabled": true }; }, "execute": function() { g_TradeDialog.toggle(); }, "allowedPlayers": ["Player"] }, "patrol": { "getInfo": function(entStates) { if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") + translate("Patrol") + "\n" + translate("Attack all encountered enemy units while avoiding structures."), "icon": "patrol.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_PATROL; }, "allowedPlayers": ["Player"] }, "share-dropsite": { "getInfo": function(entStates) { let sharableEntities = entStates.filter( entState => entState.resourceDropsite && entState.resourceDropsite.sharable); if (!sharableEntities.length) return false; // Returns if none of the entities belong to a player with a mutual ally. if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some( (isAlly, playerId) => isAlly && playerId != entState.player))) return false; return sharableEntities.some(entState => !entState.resourceDropsite.shared) ? { "tooltip": translate("Press to allow allies to use this dropsite"), "icon": "locked_small.png", "enabled": true } : { "tooltip": translate("Press to prevent allies from using this dropsite"), "icon": "unlocked_small.png", "enabled": true }; }, "execute": function(entStates) { let sharableEntities = entStates.filter( entState => entState.resourceDropsite && entState.resourceDropsite.sharable); if (sharableEntities) Engine.PostNetworkCommand({ "type": "set-dropsite-sharing", "entities": sharableEntities.map(entState => entState.id), "shared": sharableEntities.some(entState => !entState.resourceDropsite.shared) }); }, "allowedPlayers": ["Player"] }, "is-dropsite-shared": { "getInfo": function(entStates) { let shareableEntities = entStates.filter( entState => entState.resourceDropsite && entState.resourceDropsite.sharable); if (!shareableEntities.length) return false; let player = Engine.GetPlayerID(); let simState = GetSimState(); if (!g_IsObserver && !simState.players[player].hasSharedDropsites || shareableEntities.every(entState => controlsPlayer(entState.player))) return false; if (!shareableEntities.every(entState => entState.resourceDropsite.shared)) return { "tooltip": translate("The use of this dropsite is prohibited"), "icon": "locked_small.png", "enabled": false }; return { "tooltip": g_IsObserver ? translate("Allies are allowed to use this dropsite.") : translate("You are allowed to use this dropsite"), "icon": "unlocked_small.png", "enabled": false }; }, "execute": function(entState) { // This command button is always disabled. }, "allowedPlayers": ["Ally", "Observer"] }, "autoqueue-on": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.production || !entState.production.entities.length || entState.production.autoqueue)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueon") + translate("Activate auto-queue for selected structures."), "icon": "autoqueue-on.png", "enabled": true }; }, "execute": function(entStates) { if (entStates.length) turnAutoQueueOn(); }, "allowedPlayers": ["Player"] }, "autoqueue-off": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.production || !entState.production.entities.length || !entState.production.autoqueue)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueoff") + translate("Deactivate auto-queue for selected structures."), "icon": "autoqueue-off.png", "enabled": true }; }, "execute": function(entStates) { if (entStates.length) turnAutoQueueOff(); }, "allowedPlayers": ["Player"] }, }; function playerCheck(entState, targetState, validPlayers) { let playerState = GetSimState().players[entState.player]; for (let player of validPlayers) if (player == "Gaia" && targetState.player == 0 || player == "Player" && targetState.player == entState.player || playerState["is" + player] && playerState["is" + player][targetState.player]) return true; return false; } /** * Checks whether the entities have the right diplomatic status * with respect to the currently active player. * Also "Observer" can be used. * * @param {Object[]} entStates - An array containing the entity states to check. * @param {string[]} validPlayers - An array containing the diplomatic statuses. * * @return {boolean} - Whether the currently active player is allowed. */ function allowedPlayersCheck(entStates, validPlayers) { // Assume we can only select entities from one player, // or it does not matter (e.g. observer). let targetState = entStates[0]; let playerState = GetSimState().players[Engine.GetPlayerID()]; return validPlayers.some(player => player == "Observer" && g_IsObserver || player == "Player" && controlsPlayer(targetState.player) || playerState && playerState["is" + player] && playerState["is" + player][targetState.player]); } function hasClass(entState, className) { // note: use the functions in globalscripts/Templates.js for more versatile matching return entState.identity && entState.identity.classes.indexOf(className) != -1; } /** * Keep in sync with Commands.js. */ function isUndeletable(entState) { let playerState = g_SimState.players[entState.player]; if (playerState && playerState.controlsAll) return false; if (entState.resourceSupply && entState.resourceSupply.killBeforeGather) return translate("The entity has to be killed before it can be gathered from"); if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2) return translate("You cannot destroy this entity as you own less than half the capture points"); if (!entState.identity.canDelete) return translate("This entity is undeletable"); return false; } function DrawTargetMarker(target) { Engine.GuiInterfaceCall("AddTargetMarker", { "template": g_TargetMarker.move, "x": target.x, "z": target.z }); } +function displayFlare(target, playerID) +{ + Engine.GuiInterfaceCall("AddTargetMarker", { + "template": g_TargetMarker.map_flare, + "x": target.x, + "z": target.z, + "owner": playerID + }); + g_MiniMapPanel.flare(target, playerID); +} + function getCommandInfo(command, entStates) { return entStates && g_EntityCommands[command] && allowedPlayersCheck(entStates, g_EntityCommands[command].allowedPlayers) && g_EntityCommands[command].getInfo(entStates); } function getActionInfo(action, target, selection) { if (!selection || !selection.length || !GetEntityState(selection[0])) return { "possible": false }; // Look at the first targeted entity // (TODO: maybe we eventually want to look at more, and be more context-sensitive? // e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse) let targetState = GetEntityState(target); let simState = GetSimState(); let playerState = g_SimState.players[g_ViewedPlayer]; // Check if any entities in the selection can do some of the available actions. for (let entityID of selection) { let entState = GetEntityState(entityID); if (!entState) continue; if (playerState && !playerState.controlsAll && !entState.identity.controllable) continue; if (g_UnitActions[action] && g_UnitActions[action].getActionInfo) { let r = g_UnitActions[action].getActionInfo(entState, targetState, simState); if (r && r.possible) { r.entity = entityID; return r; } } } return { "possible": false }; } Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/flare_target_marker.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/flare_target_marker.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/flare_target_marker.xml (revision 25691) @@ -0,0 +1,17 @@ + + + + true + false + 6 + 10000 + 0 + + + + true + + + special/flare_target_marker.xml + + Index: ps/trunk/source/gui/ObjectTypes/CMiniMap.h =================================================================== --- ps/trunk/source/gui/ObjectTypes/CMiniMap.h (revision 25690) +++ ps/trunk/source/gui/ObjectTypes/CMiniMap.h (revision 25691) @@ -1,88 +1,120 @@ /* Copyright (C) 2021 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_MINIMAP #define INCLUDED_MINIMAP +#include "graphics/Color.h" +#include "graphics/Texture.h" #include "gui/ObjectBases/IGUIObject.h" +#include "maths/Vector2D.h" #include "renderer/VertexArray.h" +#include +#include + class CCamera; class CMatrix3D; class CTerrain; class CMiniMap : public IGUIObject { GUI_OBJECT(CMiniMap) + public: CMiniMap(CGUI& pGUI); virtual ~CMiniMap(); + bool Flare(const CVector2D& pos, const CStr& colorStr); + protected: + struct MapFlare + { + CVector2D pos; + CColor color; + double time; + }; + virtual void Draw(CCanvas2D& canvas); + virtual void CreateJSObject(); + /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage& Message); /** * @see IGUIObject#IsMouseOver() */ virtual bool IsMouseOver() const; private: void SetCameraPositionFromMousePosition(); bool FireWorldClickEvent(int button, int clicks); static const CStr EventNameWorldClick; const CCamera* m_Camera; // Whether or not the mouse is currently down bool m_Clicking; + std::deque m_MapFlares; + + std::vector m_FlareTextures; + + CGUISimpleSetting m_FlareTextureCount; + CGUISimpleSetting m_FlareRenderSize; + CGUISimpleSetting m_FlareAnimationSpeed; + CGUISimpleSetting m_FlareInterleave; + CGUISimpleSetting m_FlareLifetimeSeconds; + // Whether to draw a black square around and under the minimap. CGUISimpleSetting m_Mask; // map size ssize_t m_MapSize; // 1.f if map is circular or 1.414f if square (to shrink it inside the circle) float m_MapScale; + void RecreateFlareTextures(); + void DrawViewRect(const CMatrix3D& transform) const; + void DrawFlare(CCanvas2D& canvas, const MapFlare& flare, double curentTime) const; + void GetMouseWorldCoordinates(float& x, float& z) const; float GetAngle() const; VertexIndexArray m_IndexArray; VertexArray m_VertexArray; VertexArray::Attribute m_AttributePos; VertexArray::Attribute m_AttributeColor; size_t m_EntitiesDrawn; double m_PingDuration; double m_HalfBlinkDuration; double m_NextBlinkTime; bool m_BlinkState; }; #endif // INCLUDED_MINIMAP