Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 21644)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 21645)
@@ -1,702 +1,725 @@
function GarrisonHolder() {}
GarrisonHolder.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
"" +
"";
/**
* Initialize GarrisonHolder Component
* Garrisoning when loading a map is set in the script of the map, by setting initGarrison
* which should contain the array of garrisoned entities.
*/
GarrisonHolder.prototype.Init = function()
{
// Garrisoned Units
this.entities = [];
this.timer = undefined;
this.allowGarrisoning = new Map();
this.visibleGarrisonPoints = [];
if (this.template.VisibleGarrisonPoints)
{
let points = this.template.VisibleGarrisonPoints;
for (let point in points)
this.visibleGarrisonPoints.push({
"offset": {
"x": +points[point].X,
"y": +points[point].Y,
"z": +points[point].Z
},
+ "angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null,
"entity": null
});
}
};
/**
* @return {Object} max and min range at which entities can garrison the holder.
*/
GarrisonHolder.prototype.GetLoadingRange = function()
{
return { "max": +this.template.LoadingRange, "min": 0 };
};
GarrisonHolder.prototype.CanPickup = function(ent)
{
if (!this.template.Pickup || this.IsFull())
return false;
let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership);
return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent);
};
GarrisonHolder.prototype.GetEntities = function()
{
return this.entities;
};
/**
* @return {Array} unit classes which can be garrisoned inside this
* particular entity. Obtained from the entity's template.
*/
GarrisonHolder.prototype.GetAllowedClasses = function()
{
return this.template.List._string;
};
GarrisonHolder.prototype.GetCapacity = function()
{
return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity);
};
GarrisonHolder.prototype.IsFull = function()
{
return this.GetGarrisonedEntitiesCount() >= this.GetCapacity();
};
GarrisonHolder.prototype.GetHealRate = function()
{
return ApplyValueModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity);
};
/**
* Set this entity to allow or disallow garrisoning in the entity.
* Every component calling this function should do it with its own ID, and as long as one
* component doesn't allow this entity to garrison, it can't be garrisoned
* When this entity already contains garrisoned soldiers,
* these will not be able to ungarrison until the flag is set to true again.
*
* This more useful for modern-day features. For example you can't garrison or ungarrison
* a driving vehicle or plane.
* @param {boolean} allow - Whether the entity should be garrisonable.
*/
GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID)
{
this.allowGarrisoning.set(callerID, allow);
};
GarrisonHolder.prototype.IsGarrisoningAllowed = function()
{
return Array.from(this.allowGarrisoning.values()).every(allow => allow);
};
GarrisonHolder.prototype.GetGarrisonedEntitiesCount = function()
{
let count = this.entities.length;
for (let ent of this.entities)
{
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
count += cmpGarrisonHolder.GetGarrisonedEntitiesCount();
}
return count;
};
GarrisonHolder.prototype.IsAllowedToGarrison = function(ent)
{
if (!this.IsGarrisoningAllowed())
return false;
if (!IsOwnedByMutualAllyOfEntity(ent, this.entity))
return false;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
return false;
let entityClasses = cmpIdentity.GetClassesList();
return MatchesClassList(entityClasses, this.template.List._string) && !!Engine.QueryInterface(ent, IID_Garrisonable);
};
/**
* Garrison a unit inside. The timer for AutoHeal is started here.
* @param {number} vgpEntity - The visual garrison point that will be used.
* If vgpEntity is given, this visualGarrisonPoint will be used for the entity.
* @return {boolean} Whether the entity was garrisonned.
*/
GarrisonHolder.prototype.Garrison = function(entity, vgpEntity)
{
let cmpPosition = Engine.QueryInterface(entity, IID_Position);
if (!cmpPosition)
return false;
if (!this.PerformGarrison(entity))
return false;
let visibleGarrisonPoint = vgpEntity;
if (!visibleGarrisonPoint)
for (let vgp of this.visibleGarrisonPoints)
{
if (vgp.entity)
continue;
visibleGarrisonPoint = vgp;
break;
}
if (visibleGarrisonPoint)
{
visibleGarrisonPoint.entity = entity;
- cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset);
+ // Angle of turrets:
+ // Renamed entities (vgpEntity != undefined) should keep their angle.
+ // Otherwise if an angle is given in the visibleGarrisonPoint, use it.
+ // If no such angle given (usually walls for which outside/inside not well defined), we keep
+ // the current angle as it was used for garrisoning and thus quite often was from inside to
+ // outside, except when garrisoning from outWorld where we take as default PI.
+ let cmpTurretPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!vgpEntity && visibleGarrisonPoint.angle != null)
+ cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + visibleGarrisonPoint.angle);
+ else if (!vgpEntity && !cmpPosition.IsInWorld())
+ cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + Math.PI);
+ let cmpUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion);
+ if (cmpUnitMotion)
+ cmpUnitMotion.SetFacePointAfterMove(false);
let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SetTurretStance();
+ cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset);
}
else
cmpPosition.MoveOutOfWorld();
return true;
};
/**
* @return {boolean} Whether the entity was garrisonned.
*/
GarrisonHolder.prototype.PerformGarrison = function(entity)
{
if (!this.HasEnoughHealth())
return false;
// Check if the unit is allowed to be garrisoned inside the building
if (!this.IsAllowedToGarrison(entity))
return false;
// Check the capacity
let extraCount = 0;
let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
extraCount += cmpGarrisonHolder.GetGarrisonedEntitiesCount();
if (this.GetGarrisonedEntitiesCount() + extraCount >= this.GetCapacity())
return false;
if (!this.timer && this.GetHealRate() > 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
}
// Actual garrisoning happens here
this.entities.push(entity);
this.UpdateGarrisonFlag();
let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.PauseProduction();
let cmpAura = Engine.QueryInterface(entity, IID_Auras);
if (cmpAura && cmpAura.HasGarrisonAura())
cmpAura.ApplyGarrisonBonus(this.entity);
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [entity], "removed": [] });
return true;
};
/**
* Simply eject the unit from the garrisoning entity without moving it
* @param {number} entity - Id of the entity to be ejected.
* @param {boolean} forced - Whether eject is forced (i.e. if building is destroyed).
* @return {boolean} Whether the entity was ejected.
*/
GarrisonHolder.prototype.Eject = function(entity, forced)
{
let entityIndex = this.entities.indexOf(entity);
// Error: invalid entity ID, usually it's already been ejected
if (entityIndex == -1)
return false;
// Find spawning location
let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
// If the garrisonHolder is a sinking ship, restrict the location to the intersection of both passabilities
// TODO: should use passability classes to be more generic
let pos;
if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship"))
pos = cmpFootprint.PickSpawnPointBothPass(entity);
else
pos = cmpFootprint.PickSpawnPoint(entity);
if (pos.y < 0)
{
// Error: couldn't find space satisfying the unit's passability criteria
if (!forced)
return false;
// If ejection is forced, we need to continue, so use center of the building
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
pos = cmpPosition.GetPosition();
}
- let cmpNewPosition = Engine.QueryInterface(entity, IID_Position);
this.entities.splice(entityIndex, 1);
+ let cmpEntPosition = Engine.QueryInterface(entity, IID_Position);
+ let cmpEntUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
- let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
for (let vgp of this.visibleGarrisonPoints)
{
if (vgp.entity != entity)
continue;
- cmpNewPosition.SetTurretParent(INVALID_ENTITY, new Vector3D());
- if (cmpUnitAI)
- cmpUnitAI.ResetTurretStance();
+ cmpEntPosition.SetTurretParent(INVALID_ENTITY, new Vector3D());
+ let cmpEntUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion);
+ if (cmpEntUnitMotion)
+ cmpEntUnitMotion.SetFacePointAfterMove(true);
+ if (cmpEntUnitAI)
+ cmpEntUnitAI.ResetTurretStance();
vgp.entity = null;
break;
}
- if (cmpUnitAI)
- cmpUnitAI.Ungarrison();
-
- let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue);
- if (cmpProductionQueue)
- cmpProductionQueue.UnpauseProduction();
+ if (cmpEntUnitAI)
+ cmpEntUnitAI.Ungarrison();
- let cmpAura = Engine.QueryInterface(entity, IID_Auras);
- if (cmpAura && cmpAura.HasGarrisonAura())
- cmpAura.RemoveGarrisonBonus(this.entity);
+ let cmpEntProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue);
+ if (cmpEntProductionQueue)
+ cmpEntProductionQueue.UnpauseProduction();
+
+ let cmpEntAura = Engine.QueryInterface(entity, IID_Auras);
+ if (cmpEntAura && cmpEntAura.HasGarrisonAura())
+ cmpEntAura.RemoveGarrisonBonus(this.entity);
- cmpNewPosition.JumpTo(pos.x, pos.z);
- cmpNewPosition.SetHeightOffset(0);
+ cmpEntPosition.JumpTo(pos.x, pos.z);
+ cmpEntPosition.SetHeightOffset(0);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition)
- cmpNewPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos));
+ cmpEntPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos));
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [entity] });
return true;
};
/**
* Ejects units and orders them to move to the rally point. If an ejection
* with a given obstruction radius has failed, we won't try anymore to eject
* entities with a bigger obstruction as that is compelled to also fail.
* @param {Array} entities - An array containing the ids of the entities to eject.
* @param {boolean} forced - Whether eject is forced (ie: if building is destroyed).
* @return {boolean} Whether the entities were ejected.
*/
GarrisonHolder.prototype.PerformEject = function(entities, forced)
{
if (!this.IsGarrisoningAllowed() && !forced)
return false;
let ejectedEntities = [];
let success = true;
let failedRadius;
let radius;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
for (let entity of entities)
{
if (failedRadius !== undefined)
{
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
radius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0;
if (radius >= failedRadius)
continue;
}
if (this.Eject(entity, forced))
{
let cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership);
if (cmpOwnership && cmpEntOwnership && cmpOwnership.GetOwner() == cmpEntOwnership.GetOwner())
ejectedEntities.push(entity);
}
else
{
success = false;
if (failedRadius !== undefined)
failedRadius = Math.min(failedRadius, radius);
else
{
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
failedRadius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0;
}
}
}
this.OrderWalkToRallyPoint(ejectedEntities);
this.UpdateGarrisonFlag();
return success;
};
/**
* Order entities to walk to the rally point.
* @param {Array} entities - An array containing all the ids of the entities.
*/
GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
if (!cmpRallyPoint || !cmpRallyPoint.GetPositions()[0])
return;
let commands = GetRallyPointCommands(cmpRallyPoint, entities);
// Ignore the rally point if it is autogarrison
if (commands[0].type == "garrison" && commands[0].target == this.entity)
return;
for (let command of commands)
ProcessCommand(cmpOwnership.GetOwner(), command);
};
/**
* Unload unit from the garrisoning entity and order them
* to move to the rally point.
* @return {boolean} Whether the command was successful.
*/
GarrisonHolder.prototype.Unload = function(entity, forced)
{
return this.PerformEject([entity], forced);
};
/**
* Unload one or all units that match a template and owner from
* the garrisoning entity and order them to move to the rally point.
* @param {string} template - Type of units that should be ejected.
* @param {number} owner - Id of the player whose units should be ejected.
* @param {boolean} all - Whether all units should be ejected.
* @param {boolean} forced - Whether unload is forced.
* @return {boolean} Whether the unloading was successful.
*/
GarrisonHolder.prototype.UnloadTemplate = function(template, owner, all, forced)
{
let entities = [];
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let entity of this.entities)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
// Units with multiple ranks are grouped together.
let name = cmpIdentity.GetSelectionGroupName() || cmpTemplateManager.GetCurrentTemplateName(entity);
if (name != template || owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner())
continue;
entities.push(entity);
// If 'all' is false, only ungarrison the first matched unit.
if (!all)
break;
}
return this.PerformEject(entities, forced);
};
/**
* Unload all units, that belong to certain player
* and order all own units to move to the rally point.
* @param {boolean} forced - Whether unload is forced.
* @param {number} owner - Id of the player whose units should be ejected.
* @return {boolean} Whether the unloading was successful.
*/
GarrisonHolder.prototype.UnloadAllByOwner = function(owner, forced)
{
let entities = this.entities.filter(ent => {
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
return cmpOwnership && cmpOwnership.GetOwner() == owner;
});
return this.PerformEject(entities, forced);
};
/**
* Unload all units from the entity and order them to move to the rally point.
* @param {boolean} forced - Whether unload is forced.
* @return {boolean} Whether the unloading was successful.
*/
GarrisonHolder.prototype.UnloadAll = function(forced)
{
return this.PerformEject(this.entities.slice(), forced);
};
/**
* Used to check if the garrisoning entity's health has fallen below
* a certain limit after which all garrisoned units are unloaded.
*/
GarrisonHolder.prototype.OnHealthChanged = function(msg)
{
if (!this.HasEnoughHealth() && this.entities.length)
this.EjectOrKill(this.entities.slice());
};
GarrisonHolder.prototype.HasEnoughHealth = function()
{
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
return cmpHealth.GetHitpoints() > Math.floor(+this.template.EjectHealth * cmpHealth.GetMaxHitpoints());
};
/**
* Called every second. Heals garrisoned units.
*/
GarrisonHolder.prototype.HealTimeout = function(data)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (!this.entities.length)
{
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
return;
}
for (let entity of this.entities)
{
let cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth && !cmpHealth.IsUnhealable())
cmpHealth.Increase(this.GetHealRate());
}
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
};
/**
* Updates the garrison flag depending whether something is garrisoned in the entity.
*/
GarrisonHolder.prototype.UpdateGarrisonFlag = function()
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetVariant("garrison", this.entities.length ? "garrisoned" : "ungarrisoned");
};
/**
* Cancel timer when destroyed.
*/
GarrisonHolder.prototype.OnDestroy = function()
{
if (this.timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
}
};
/**
* If a garrisoned entity is captured, or about to be killed (so its owner changes to '-1'),
* remove it from the building so we only ever contain valid entities.
*/
GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg)
{
// The ownership change may be on the garrisonholder
if (this.entity == msg.entity)
{
let entities = this.entities.filter(ent => msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, ent));
if (entities.length)
this.EjectOrKill(entities);
return;
}
// or on some of its garrisoned units
let entityIndex = this.entities.indexOf(msg.entity);
if (entityIndex != -1)
{
// If the entity is dead, remove it directly instead of ejecting the corpse
let cmpHealth = Engine.QueryInterface(msg.entity, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() == 0)
{
this.entities.splice(entityIndex, 1);
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [msg.entity] });
this.UpdateGarrisonFlag();
for (let point of this.visibleGarrisonPoints)
if (point.entity == msg.entity)
point.entity = null;
}
else if (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity))
this.EjectOrKill([msg.entity]);
}
};
/**
* Update list of garrisoned entities if one gets renamed (e.g. by promotion).
*/
GarrisonHolder.prototype.OnGlobalEntityRenamed = function(msg)
{
let entityIndex = this.entities.indexOf(msg.entity);
if (entityIndex != -1)
{
let vgpRenamed;
for (let vgp of this.visibleGarrisonPoints)
{
if (vgp.entity != msg.entity)
continue;
vgpRenamed = vgp;
break;
}
this.Eject(msg.entity, true);
this.Garrison(msg.newentity, vgpRenamed);
}
if (!this.initGarrison)
return;
// Update the pre-game garrison because of SkirmishReplacement
if (msg.entity == this.entity)
{
let cmpGarrisonHolder = Engine.QueryInterface(msg.newentity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
cmpGarrisonHolder.initGarrison = this.initGarrison;
}
else
{
entityIndex = this.initGarrison.indexOf(msg.entity);
if (entityIndex != -1)
this.initGarrison[entityIndex] = msg.newentity;
}
};
/**
* Eject all foreign garrisoned entities which are no more allied.
*/
GarrisonHolder.prototype.OnDiplomacyChanged = function()
{
this.EjectOrKill(this.entities.filter(ent => !IsOwnedByMutualAllyOfEntity(this.entity, ent)));
};
/**
* Eject or kill a garrisoned unit which can no more be garrisoned
* (garrisonholder's health too small or ownership changed).
*/
GarrisonHolder.prototype.EjectOrKill = function(entities)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
// Eject the units which can be ejected (if not in world, it generally means this holder
// is inside a holder which kills its entities, so do not eject)
if (cmpPosition && cmpPosition.IsInWorld())
{
let ejectables = entities.filter(ent => this.IsEjectable(ent));
if (ejectables.length)
this.PerformEject(ejectables, false);
}
// And destroy all remaining entities
let killedEntities = [];
for (let entity of entities)
{
let entityIndex = this.entities.indexOf(entity);
if (entityIndex == -1)
continue;
let cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
this.entities.splice(entityIndex, 1);
killedEntities.push(entity);
}
if (killedEntities.length)
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": killedEntities });
this.UpdateGarrisonFlag();
};
GarrisonHolder.prototype.IsEjectable = function(entity)
{
if (!this.entities.find(ent => ent == entity))
return false;
let ejectableClasses = this.template.EjectClassesOnDestroy._string;
ejectableClasses = ejectableClasses ? ejectableClasses.split(/\s+/) : [];
let entityClasses = Engine.QueryInterface(entity, IID_Identity).GetClassesList();
return ejectableClasses.some(ejectableClass => entityClasses.indexOf(ejectableClass) != -1);
};
/**
* Initialise the garrisoned units.
*/
GarrisonHolder.prototype.OnGlobalInitGame = function(msg)
{
if (!this.initGarrison)
return;
for (let ent of this.initGarrison)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.CanGarrison(this.entity) && this.Garrison(ent))
cmpUnitAI.Autogarrison(this.entity);
}
this.initGarrison = undefined;
};
GarrisonHolder.prototype.OnValueModification = function(msg)
{
if (msg.component != "GarrisonHolder" || msg.valueNames.indexOf("GarrisonHolder/BuffHeal") == -1)
return;
if (this.timer && this.GetHealRate() == 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
else if (!this.timer && this.GetHealRate() > 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
}
};
Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder);
Index: ps/trunk/source/simulation2/components/CCmpPosition.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpPosition.cpp (revision 21644)
+++ ps/trunk/source/simulation2/components/CCmpPosition.cpp (revision 21645)
@@ -1,979 +1,981 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "simulation2/system/Component.h"
#include "ICmpPosition.h"
#include "simulation2/MessageTypes.h"
#include "ICmpTerrain.h"
#include "ICmpTerritoryManager.h"
#include "ICmpVisual.h"
#include "ICmpWaterManager.h"
#include "graphics/Terrain.h"
#include "lib/rand.h"
#include "maths/MathUtil.h"
#include "maths/Matrix3D.h"
#include "maths/Vector3D.h"
#include "maths/Vector2D.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
/**
* Basic ICmpPosition implementation.
*/
class CCmpPosition : public ICmpPosition
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_TurnStart);
componentManager.SubscribeToMessageType(MT_TerrainChanged);
componentManager.SubscribeToMessageType(MT_WaterChanged);
componentManager.SubscribeToMessageType(MT_Deserialized);
// TODO: if this component turns out to be a performance issue, it should
// be optimised by creating a new PositionStatic component that doesn't subscribe
// to messages and doesn't store LastX/LastZ, and that should be used for all
// entities that don't move
}
DEFAULT_COMPONENT_ALLOCATOR(Position)
// Template state:
enum
{
UPRIGHT = 0,
PITCH = 1,
PITCH_ROLL = 2,
ROLL = 3,
} m_AnchorType;
bool m_Floating;
entity_pos_t m_FloatDepth;
float m_RotYSpeed; // maximum radians per second, used by InterpolatedRotY to follow RotY
// Dynamic state:
bool m_InWorld;
// m_LastX/Z contain the position from the start of the most recent turn
// m_PrevX/Z conatain the position from the turn before that
entity_pos_t m_X, m_Z, m_LastX, m_LastZ, m_PrevX, m_PrevZ; // these values contain undefined junk if !InWorld
entity_pos_t m_Y, m_LastYDifference; // either the relative or the absolute Y coordinate
bool m_RelativeToGround; // whether m_Y is relative to terrain/water plane, or an absolute height
fixed m_ConstructionProgress;
// when the entity is a turret, only m_RotY is used, and this is the rotation
// relative to the parent entity
entity_angle_t m_RotX, m_RotY, m_RotZ;
player_id_t m_Territory;
entity_id_t m_TurretParent;
CFixedVector3D m_TurretPosition;
std::set m_Turrets;
// Not serialized:
float m_InterpolatedRotX, m_InterpolatedRotY, m_InterpolatedRotZ;
float m_LastInterpolatedRotX, m_LastInterpolatedRotZ;
bool m_ActorFloating;
bool m_EnabledMessageInterpolate;
static std::string GetSchema()
{
return
"Allows this entity to exist at a location (and orientation) in the world, and defines some details of the positioning."
""
"upright"
"0.0"
"false"
"0.0"
"6.0"
""
""
""
"upright"
"pitch"
"roll"
"pitch-roll"
""
""
""
""
""
""
""
""
""
""
""
""
""
"";
}
virtual void Init(const CParamNode& paramNode)
{
std::wstring anchor = paramNode.GetChild("Anchor").ToString();
if (anchor == L"pitch")
m_AnchorType = PITCH;
else if (anchor == L"pitch-roll")
m_AnchorType = PITCH_ROLL;
else if (anchor == L"roll")
m_AnchorType = ROLL;
else
m_AnchorType = UPRIGHT;
m_InWorld = false;
m_LastYDifference = entity_pos_t::Zero();
m_Y = paramNode.GetChild("Altitude").ToFixed();
m_RelativeToGround = true;
m_Floating = paramNode.GetChild("Floating").ToBool();
m_FloatDepth = paramNode.GetChild("FloatDepth").ToFixed();
m_RotYSpeed = paramNode.GetChild("TurnRate").ToFixed().ToFloat();
m_RotX = m_RotY = m_RotZ = entity_angle_t::FromInt(0);
m_InterpolatedRotX = m_InterpolatedRotY = m_InterpolatedRotZ = 0.f;
m_LastInterpolatedRotX = m_LastInterpolatedRotZ = 0.f;
m_Territory = INVALID_PLAYER;
m_TurretParent = INVALID_ENTITY;
m_TurretPosition = CFixedVector3D();
m_ActorFloating = false;
m_EnabledMessageInterpolate = false;
}
virtual void Deinit()
{
}
virtual void Serialize(ISerializer& serialize)
{
serialize.Bool("in world", m_InWorld);
if (m_InWorld)
{
serialize.NumberFixed_Unbounded("x", m_X);
serialize.NumberFixed_Unbounded("y", m_Y);
serialize.NumberFixed_Unbounded("z", m_Z);
serialize.NumberFixed_Unbounded("last x", m_LastX);
serialize.NumberFixed_Unbounded("last y diff", m_LastYDifference);
serialize.NumberFixed_Unbounded("last z", m_LastZ);
}
serialize.NumberI32_Unbounded("territory", m_Territory);
serialize.NumberFixed_Unbounded("rot x", m_RotX);
serialize.NumberFixed_Unbounded("rot y", m_RotY);
serialize.NumberFixed_Unbounded("rot z", m_RotZ);
serialize.NumberFixed_Unbounded("altitude", m_Y);
serialize.Bool("relative", m_RelativeToGround);
serialize.Bool("floating", m_Floating);
serialize.NumberFixed_Unbounded("float depth", m_FloatDepth);
serialize.NumberFixed_Unbounded("constructionprogress", m_ConstructionProgress);
if (serialize.IsDebug())
{
const char* anchor = "???";
switch (m_AnchorType)
{
case PITCH:
anchor = "pitch";
break;
case PITCH_ROLL:
anchor = "pitch-roll";
break;
case ROLL:
anchor = "roll";
break;
case UPRIGHT: // upright is the default
default:
anchor = "upright";
break;
}
serialize.StringASCII("anchor", anchor, 0, 16);
}
serialize.NumberU32_Unbounded("turret parent", m_TurretParent);
if (m_TurretParent != INVALID_ENTITY)
{
serialize.NumberFixed_Unbounded("x", m_TurretPosition.X);
serialize.NumberFixed_Unbounded("y", m_TurretPosition.Y);
serialize.NumberFixed_Unbounded("z", m_TurretPosition.Z);
}
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
deserialize.Bool("in world", m_InWorld);
if (m_InWorld)
{
deserialize.NumberFixed_Unbounded("x", m_X);
deserialize.NumberFixed_Unbounded("y", m_Y);
deserialize.NumberFixed_Unbounded("z", m_Z);
deserialize.NumberFixed_Unbounded("last x", m_LastX);
deserialize.NumberFixed_Unbounded("last y diff", m_LastYDifference);
deserialize.NumberFixed_Unbounded("last z", m_LastZ);
}
deserialize.NumberI32_Unbounded("territory", m_Territory);
deserialize.NumberFixed_Unbounded("rot x", m_RotX);
deserialize.NumberFixed_Unbounded("rot y", m_RotY);
deserialize.NumberFixed_Unbounded("rot z", m_RotZ);
deserialize.NumberFixed_Unbounded("altitude", m_Y);
deserialize.Bool("relative", m_RelativeToGround);
deserialize.Bool("floating", m_Floating);
deserialize.NumberFixed_Unbounded("float depth", m_FloatDepth);
deserialize.NumberFixed_Unbounded("constructionprogress", m_ConstructionProgress);
// TODO: should there be range checks on all these values?
m_InterpolatedRotY = m_RotY.ToFloat();
deserialize.NumberU32_Unbounded("turret parent", m_TurretParent);
if (m_TurretParent != INVALID_ENTITY)
{
deserialize.NumberFixed_Unbounded("x", m_TurretPosition.X);
deserialize.NumberFixed_Unbounded("y", m_TurretPosition.Y);
deserialize.NumberFixed_Unbounded("z", m_TurretPosition.Z);
}
if (m_InWorld)
UpdateXZRotation();
UpdateMessageSubscriptions();
}
void Deserialized()
{
AdvertiseInterpolatedPositionChanges();
}
virtual void UpdateTurretPosition()
{
if (m_TurretParent == INVALID_ENTITY)
return;
CmpPtr cmpPosition(GetSimContext(), m_TurretParent);
if (!cmpPosition)
{
LOGERROR("Turret with parent without position component");
return;
}
if (!cmpPosition->IsInWorld())
MoveOutOfWorld();
else
{
CFixedVector2D rotatedPosition = CFixedVector2D(m_TurretPosition.X, m_TurretPosition.Z);
rotatedPosition = rotatedPosition.Rotate(cmpPosition->GetRotation().Y);
CFixedVector2D rootPosition = cmpPosition->GetPosition2D();
entity_pos_t x = rootPosition.X + rotatedPosition.X;
entity_pos_t z = rootPosition.Y + rotatedPosition.Y;
if (!m_InWorld || m_X != x || m_Z != z)
MoveTo(x, z);
entity_pos_t y = cmpPosition->GetHeightOffset() + m_TurretPosition.Y;
if (!m_InWorld || GetHeightOffset() != y)
SetHeightOffset(y);
m_InWorld = true;
}
}
virtual std::set* GetTurrets()
{
return &m_Turrets;
}
virtual void SetTurretParent(entity_id_t id, const CFixedVector3D& offset)
{
+ entity_angle_t angle = GetRotation().Y;
if (m_TurretParent != INVALID_ENTITY)
{
CmpPtr cmpPosition(GetSimContext(), m_TurretParent);
if (cmpPosition)
cmpPosition->GetTurrets()->erase(GetEntityId());
}
m_TurretParent = id;
m_TurretPosition = offset;
if (m_TurretParent != INVALID_ENTITY)
{
CmpPtr cmpPosition(GetSimContext(), m_TurretParent);
if (cmpPosition)
cmpPosition->GetTurrets()->insert(GetEntityId());
}
+ SetYRotation(angle);
UpdateTurretPosition();
}
virtual entity_id_t GetTurretParent() const
{
return m_TurretParent;
}
virtual bool IsInWorld() const
{
return m_InWorld;
}
virtual void MoveOutOfWorld()
{
m_InWorld = false;
AdvertisePositionChanges();
AdvertiseInterpolatedPositionChanges();
}
virtual void MoveTo(entity_pos_t x, entity_pos_t z)
{
m_X = x;
m_Z = z;
if (!m_InWorld)
{
m_InWorld = true;
m_LastX = m_PrevX = m_X;
m_LastZ = m_PrevZ = m_Z;
m_LastYDifference = entity_pos_t::Zero();
}
AdvertisePositionChanges();
AdvertiseInterpolatedPositionChanges();
}
virtual void MoveAndTurnTo(entity_pos_t x, entity_pos_t z, entity_angle_t ry)
{
m_X = x;
m_Z = z;
if (!m_InWorld)
{
m_InWorld = true;
m_LastX = m_PrevX = m_X;
m_LastZ = m_PrevZ = m_Z;
m_LastYDifference = entity_pos_t::Zero();
}
// TurnTo will advertise the position changes
TurnTo(ry);
AdvertiseInterpolatedPositionChanges();
}
virtual void JumpTo(entity_pos_t x, entity_pos_t z)
{
m_LastX = m_PrevX = m_X = x;
m_LastZ = m_PrevZ = m_Z = z;
m_InWorld = true;
UpdateXZRotation();
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
AdvertisePositionChanges();
AdvertiseInterpolatedPositionChanges();
}
virtual void SetHeightOffset(entity_pos_t dy)
{
// subtract the offset and replace with a new offset
m_LastYDifference = dy - GetHeightOffset();
m_Y += m_LastYDifference;
AdvertiseInterpolatedPositionChanges();
}
virtual entity_pos_t GetHeightOffset() const
{
if (m_RelativeToGround)
return m_Y;
// not relative to the ground, so the height offset is m_Y - ground height
// except when floating, when the height offset is m_Y - water level + float depth
entity_pos_t baseY;
CmpPtr cmpTerrain(GetSystemEntity());
if (cmpTerrain)
baseY = cmpTerrain->GetGroundLevel(m_X, m_Z);
if (m_Floating)
{
CmpPtr cmpWaterManager(GetSystemEntity());
if (cmpWaterManager)
baseY = std::max(baseY, cmpWaterManager->GetWaterLevel(m_X, m_Z) - m_FloatDepth);
}
return m_Y - baseY;
}
virtual void SetHeightFixed(entity_pos_t y)
{
// subtract the absolute height and replace it with a new absolute height
m_LastYDifference = y - GetHeightFixed();
m_Y += m_LastYDifference;
AdvertiseInterpolatedPositionChanges();
}
virtual entity_pos_t GetHeightFixed() const
{
if (!m_RelativeToGround)
return m_Y;
// relative to the ground, so the fixed height = ground height + m_Y
// except when floating, when the fixed height = water level - float depth + m_Y
entity_pos_t baseY;
CmpPtr cmpTerrain(GetSystemEntity());
if (cmpTerrain)
baseY = cmpTerrain->GetGroundLevel(m_X, m_Z);
if (m_Floating)
{
CmpPtr cmpWaterManager(GetSystemEntity());
if (cmpWaterManager)
baseY = std::max(baseY, cmpWaterManager->GetWaterLevel(m_X, m_Z) - m_FloatDepth);
}
return m_Y + baseY;
}
virtual bool IsHeightRelative() const
{
return m_RelativeToGround;
}
virtual void SetHeightRelative(bool relative)
{
// move y to use the right offset (from terrain or from map origin)
m_Y = relative ? GetHeightOffset() : GetHeightFixed();
m_RelativeToGround = relative;
m_LastYDifference = entity_pos_t::Zero();
AdvertiseInterpolatedPositionChanges();
}
virtual bool CanFloat() const
{
return m_Floating;
}
virtual void SetFloating(bool flag)
{
m_Floating = flag;
AdvertiseInterpolatedPositionChanges();
}
virtual void SetActorFloating(bool flag)
{
m_ActorFloating = flag;
AdvertiseInterpolatedPositionChanges();
}
virtual void SetConstructionProgress(fixed progress)
{
m_ConstructionProgress = progress;
AdvertiseInterpolatedPositionChanges();
}
virtual CFixedVector3D GetPosition() const
{
if (!m_InWorld)
{
LOGERROR("CCmpPosition::GetPosition called on entity when IsInWorld is false");
return CFixedVector3D();
}
return CFixedVector3D(m_X, GetHeightFixed(), m_Z);
}
virtual CFixedVector2D GetPosition2D() const
{
if (!m_InWorld)
{
LOGERROR("CCmpPosition::GetPosition2D called on entity when IsInWorld is false");
return CFixedVector2D();
}
return CFixedVector2D(m_X, m_Z);
}
virtual CFixedVector3D GetPreviousPosition() const
{
if (!m_InWorld)
{
LOGERROR("CCmpPosition::GetPreviousPosition called on entity when IsInWorld is false");
return CFixedVector3D();
}
return CFixedVector3D(m_PrevX, GetHeightFixed(), m_PrevZ);
}
virtual CFixedVector2D GetPreviousPosition2D() const
{
if (!m_InWorld)
{
LOGERROR("CCmpPosition::GetPreviousPosition2D called on entity when IsInWorld is false");
return CFixedVector2D();
}
return CFixedVector2D(m_PrevX, m_PrevZ);
}
virtual void TurnTo(entity_angle_t y)
{
if (m_TurretParent != INVALID_ENTITY)
{
CmpPtr cmpPosition(GetSimContext(), m_TurretParent);
if (cmpPosition)
y -= cmpPosition->GetRotation().Y;
}
m_RotY = y;
AdvertisePositionChanges();
UpdateMessageSubscriptions();
}
virtual void SetYRotation(entity_angle_t y)
{
if (m_TurretParent != INVALID_ENTITY)
{
CmpPtr cmpPosition(GetSimContext(), m_TurretParent);
if (cmpPosition)
y -= cmpPosition->GetRotation().Y;
}
m_RotY = y;
m_InterpolatedRotY = m_RotY.ToFloat();
if (m_InWorld)
{
UpdateXZRotation();
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
}
AdvertisePositionChanges();
UpdateMessageSubscriptions();
}
virtual void SetXZRotation(entity_angle_t x, entity_angle_t z)
{
m_RotX = x;
m_RotZ = z;
if (m_InWorld)
{
UpdateXZRotation();
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
}
}
virtual CFixedVector3D GetRotation() const
{
entity_angle_t y = m_RotY;
if (m_TurretParent != INVALID_ENTITY)
{
CmpPtr cmpPosition(GetSimContext(), m_TurretParent);
if (cmpPosition)
y += cmpPosition->GetRotation().Y;
}
return CFixedVector3D(m_RotX, y, m_RotZ);
}
virtual fixed GetDistanceTravelled() const
{
if (!m_InWorld)
{
LOGERROR("CCmpPosition::GetDistanceTravelled called on entity when IsInWorld is false");
return fixed::Zero();
}
return CFixedVector2D(m_X - m_LastX, m_Z - m_LastZ).Length();
}
float GetConstructionProgressOffset(const CVector3D& pos) const
{
if (m_ConstructionProgress.IsZero())
return 0.0f;
CmpPtr cmpVisual(GetEntityHandle());
if (!cmpVisual)
return 0.0f;
// We use selection boxes to calculate the model size, since the model could be offset
// TODO: this annoyingly shows decals, would be nice to hide them
CBoundingBoxOriented bounds = cmpVisual->GetSelectionBox();
if (bounds.IsEmpty())
return 0.0f;
float dy = 2.0f * bounds.m_HalfSizes.Y;
// If this is a floating unit, we want it to start all the way under the terrain,
// so find the difference between its current position and the terrain
CmpPtr cmpTerrain(GetSystemEntity());
if (cmpTerrain && (m_Floating || m_ActorFloating))
{
float ground = cmpTerrain->GetExactGroundLevel(pos.X, pos.Z);
dy += std::max(0.f, pos.Y - ground);
}
return (m_ConstructionProgress.ToFloat() - 1.0f) * dy;
}
virtual void GetInterpolatedPosition2D(float frameOffset, float& x, float& z, float& rotY) const
{
if (!m_InWorld)
{
LOGERROR("CCmpPosition::GetInterpolatedPosition2D called on entity when IsInWorld is false");
return;
}
x = Interpolate(m_LastX.ToFloat(), m_X.ToFloat(), frameOffset);
z = Interpolate(m_LastZ.ToFloat(), m_Z.ToFloat(), frameOffset);
rotY = m_InterpolatedRotY;
}
virtual CMatrix3D GetInterpolatedTransform(float frameOffset) const
{
if (m_TurretParent != INVALID_ENTITY)
{
CmpPtr cmpPosition(GetSimContext(), m_TurretParent);
if (!cmpPosition)
{
LOGERROR("Turret with parent without position component");
CMatrix3D m;
m.SetIdentity();
return m;
}
if (!cmpPosition->IsInWorld())
{
LOGERROR("CCmpPosition::GetInterpolatedTransform called on turret entity when IsInWorld is false");
CMatrix3D m;
m.SetIdentity();
return m;
}
else
{
CMatrix3D parentTransformMatrix = cmpPosition->GetInterpolatedTransform(frameOffset);
CMatrix3D ownTransformation = CMatrix3D();
ownTransformation.SetYRotation(m_InterpolatedRotY);
ownTransformation.Translate(-m_TurretPosition.X.ToFloat(), m_TurretPosition.Y.ToFloat(), -m_TurretPosition.Z.ToFloat());
return parentTransformMatrix * ownTransformation;
}
}
if (!m_InWorld)
{
LOGERROR("CCmpPosition::GetInterpolatedTransform called on entity when IsInWorld is false");
CMatrix3D m;
m.SetIdentity();
return m;
}
float x, z, rotY;
GetInterpolatedPosition2D(frameOffset, x, z, rotY);
float baseY = 0;
if (m_RelativeToGround)
{
CmpPtr cmpTerrain(GetSystemEntity());
if (cmpTerrain)
baseY = cmpTerrain->GetExactGroundLevel(x, z);
if (m_Floating || m_ActorFloating)
{
CmpPtr cmpWaterManager(GetSystemEntity());
if (cmpWaterManager)
baseY = std::max(baseY, cmpWaterManager->GetExactWaterLevel(x, z) - m_FloatDepth.ToFloat());
}
}
float y = baseY + m_Y.ToFloat() + Interpolate(-1 * m_LastYDifference.ToFloat(), 0.f, frameOffset);
CMatrix3D m;
// linear interpolation is good enough (for RotX/Z).
// As you always stay close to zero angle.
m.SetXRotation(Interpolate(m_LastInterpolatedRotX, m_InterpolatedRotX, frameOffset));
m.RotateZ(Interpolate(m_LastInterpolatedRotZ, m_InterpolatedRotZ, frameOffset));
CVector3D pos(x, y, z);
pos.Y += GetConstructionProgressOffset(pos);
m.RotateY(rotY + (float)M_PI);
m.Translate(pos);
return m;
}
void GetInterpolatedPositions(CVector3D& pos0, CVector3D& pos1) const
{
float baseY0 = 0;
float baseY1 = 0;
float x0 = m_LastX.ToFloat();
float z0 = m_LastZ.ToFloat();
float x1 = m_X.ToFloat();
float z1 = m_Z.ToFloat();
if (m_RelativeToGround)
{
CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
if (cmpTerrain)
{
baseY0 = cmpTerrain->GetExactGroundLevel(x0, z0);
baseY1 = cmpTerrain->GetExactGroundLevel(x1, z1);
}
if (m_Floating || m_ActorFloating)
{
CmpPtr cmpWaterManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpWaterManager)
{
baseY0 = std::max(baseY0, cmpWaterManager->GetExactWaterLevel(x0, z0) - m_FloatDepth.ToFloat());
baseY1 = std::max(baseY1, cmpWaterManager->GetExactWaterLevel(x1, z1) - m_FloatDepth.ToFloat());
}
}
}
float y0 = baseY0 + m_Y.ToFloat() + m_LastYDifference.ToFloat();
float y1 = baseY1 + m_Y.ToFloat();
pos0 = CVector3D(x0, y0, z0);
pos1 = CVector3D(x1, y1, z1);
pos0.Y += GetConstructionProgressOffset(pos0);
pos1.Y += GetConstructionProgressOffset(pos1);
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Interpolate:
{
PROFILE("Position::Interpolate");
const CMessageInterpolate& msgData = static_cast (msg);
float rotY = m_RotY.ToFloat();
if (rotY != m_InterpolatedRotY)
{
float delta = rotY - m_InterpolatedRotY;
// Wrap delta to -M_PI..M_PI
delta = fmodf(delta + (float)M_PI, 2*(float)M_PI); // range -2PI..2PI
if (delta < 0) delta += 2*(float)M_PI; // range 0..2PI
delta -= (float)M_PI; // range -M_PI..M_PI
// Clamp to max rate
float deltaClamped = clamp(delta, -m_RotYSpeed*msgData.deltaSimTime, +m_RotYSpeed*msgData.deltaSimTime);
// Calculate new orientation, in a peculiar way in order to make sure the
// result gets close to m_orientation (rather than being n*2*M_PI out)
m_InterpolatedRotY = rotY + deltaClamped - delta;
// update the visual XZ rotation
if (m_InWorld)
{
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
UpdateXZRotation();
}
UpdateMessageSubscriptions();
}
break;
}
case MT_TurnStart:
{
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
if (m_InWorld && (m_LastX != m_X || m_LastZ != m_Z))
UpdateXZRotation();
// Store the positions from the turn before
m_PrevX = m_LastX;
m_PrevZ = m_LastZ;
m_LastX = m_X;
m_LastZ = m_Z;
m_LastYDifference = entity_pos_t::Zero();
// warn when a position change also causes a territory change under the entity
if (m_InWorld)
{
player_id_t newTerritory;
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (cmpTerritoryManager)
newTerritory = cmpTerritoryManager->GetOwner(m_X, m_Z);
else
newTerritory = INVALID_PLAYER;
if (newTerritory != m_Territory)
{
m_Territory = newTerritory;
CMessageTerritoryPositionChanged msg(GetEntityId(), m_Territory);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
}
else if (m_Territory != INVALID_PLAYER)
{
m_Territory = INVALID_PLAYER;
CMessageTerritoryPositionChanged msg(GetEntityId(), m_Territory);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
break;
}
case MT_TerrainChanged:
case MT_WaterChanged:
{
AdvertiseInterpolatedPositionChanges();
break;
}
case MT_Deserialized:
{
Deserialized();
break;
}
}
}
private:
/*
* Must be called whenever m_RotY or m_InterpolatedRotY change,
* to determine whether we need to call Interpolate to make the unit rotate.
*/
void UpdateMessageSubscriptions()
{
bool needInterpolate = false;
float rotY = m_RotY.ToFloat();
if (rotY != m_InterpolatedRotY)
needInterpolate = true;
if (needInterpolate != m_EnabledMessageInterpolate)
{
GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_Interpolate, this, needInterpolate);
m_EnabledMessageInterpolate = needInterpolate;
}
}
/**
* This must be called after changing anything that will affect the
* return value of GetPosition2D() or GetRotation().Y:
* - m_InWorld
* - m_X, m_Z
* - m_RotY
*/
void AdvertisePositionChanges() const
{
for (std::set::const_iterator it = m_Turrets.begin(); it != m_Turrets.end(); ++it)
{
CmpPtr cmpPosition(GetSimContext(), *it);
if (cmpPosition)
cmpPosition->UpdateTurretPosition();
}
if (m_InWorld)
{
CMessagePositionChanged msg(GetEntityId(), true, m_X, m_Z, m_RotY);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
else
{
CMessagePositionChanged msg(GetEntityId(), false, entity_pos_t::Zero(), entity_pos_t::Zero(), entity_angle_t::Zero());
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
}
/**
* This must be called after changing anything that will affect the
* return value of GetInterpolatedPositions():
* - m_InWorld
* - m_X, m_Z
* - m_LastX, m_LastZ
* - m_Y, m_LastYDifference, m_RelativeToGround
* - If m_RelativeToGround, then the ground under this unit
* - If m_RelativeToGround && m_Float, then the water level
*/
void AdvertiseInterpolatedPositionChanges() const
{
if (m_InWorld)
{
CVector3D pos0, pos1;
GetInterpolatedPositions(pos0, pos1);
CMessageInterpolatedPositionChanged msg(GetEntityId(), true, pos0, pos1);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
else
{
CMessageInterpolatedPositionChanged msg(GetEntityId(), false, CVector3D(), CVector3D());
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
}
void UpdateXZRotation()
{
if (!m_InWorld)
{
LOGERROR("CCmpPosition::UpdateXZRotation called on entity when IsInWorld is false");
return;
}
if (m_AnchorType == UPRIGHT || !m_RotZ.IsZero() || !m_RotX.IsZero())
{
// set the visual rotations to the ones fixed by the interface
m_InterpolatedRotX = m_RotX.ToFloat();
m_InterpolatedRotZ = m_RotZ.ToFloat();
return;
}
CmpPtr cmpTerrain(GetSystemEntity());
if (!cmpTerrain || !cmpTerrain->IsLoaded())
{
LOGERROR("Terrain not loaded");
return;
}
// TODO: average normal (average all the tiles?) for big units or for buildings?
CVector3D normal = cmpTerrain->CalcExactNormal(m_X.ToFloat(), m_Z.ToFloat());
// rotate the normal so the positive x direction is in the direction of the unit
CVector2D projected = CVector2D(normal.X, normal.Z);
projected.Rotate(m_InterpolatedRotY);
normal.X = projected.X;
normal.Z = projected.Y;
// project and calculate the angles
if (m_AnchorType == PITCH || m_AnchorType == PITCH_ROLL)
m_InterpolatedRotX = -atan2(normal.Z, normal.Y);
if (m_AnchorType == ROLL || m_AnchorType == PITCH_ROLL)
m_InterpolatedRotZ = atan2(normal.X, normal.Y);
}
};
REGISTER_COMPONENT_TYPE(Position)