Index: ps/trunk/binaries/data/mods/public/art/actors/props/units/weapons/rock_explosion.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/art/actors/props/units/weapons/rock_explosion.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/actors/props/units/weapons/rock_explosion.xml (revision 20676)
@@ -0,0 +1,15 @@
+
+
+
+
+
+ props/onager_projectile.dae
+
+
+
+
+
+
+
+
+
Index: ps/trunk/binaries/data/mods/public/art/actors/props/units/weapons/rock_flaming.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/art/actors/props/units/weapons/rock_flaming.xml (revision 20675)
+++ ps/trunk/binaries/data/mods/public/art/actors/props/units/weapons/rock_flaming.xml (revision 20676)
@@ -1,14 +1,16 @@
props/onager_projectile.dae
-
+
+
+
Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 20675)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 20676)
@@ -1,638 +1,692 @@
function Attack() {}
var g_AttackTypes = ["Melee", "Ranged", "Capture"];
Attack.prototype.bonusesSchema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Attack.prototype.preferredClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
Attack.prototype.restrictedClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
Attack.prototype.Schema =
"Controls the attack abilities and strengths of the unit." +
"" +
"" +
"10.0" +
"0.0" +
"5.0" +
"4.0" +
"1000" +
"" +
"" +
"pers" +
"Infantry" +
"1.5" +
"" +
"" +
"Cavalry Melee" +
"1.5" +
"" +
"" +
"Champion" +
"Cavalry Infantry" +
"" +
"" +
"0.0" +
"10.0" +
"0.0" +
"44.0" +
"20.0" +
"15.0" +
"800" +
"1600" +
"50.0" +
"2.5" +
"1000" +
"" +
"" +
"Cavalry" +
"2" +
"" +
"" +
+ "" +
+ "props/units/weapons/rock_flaming.xml" +
+ "props/units/weapons/rock_explosion.xml" +
+ "0.1" +
+ "" +
"Champion" +
"" +
"Circular" +
"20" +
"false" +
"0.0" +
"10.0" +
"0.0" +
"" +
"" +
"" +
"1000.0" +
"0.0" +
"0.0" +
"4.0" +
"" +
"" +
"" +
"" +
"" +
DamageTypes.BuildSchema("damage strength") +
"" +
"" +
"" +
"" +
"" + // TODO: it shouldn't be stretched
"" +
"" +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
DamageTypes.BuildSchema("damage strength") +
"" +
"" +
""+
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
"" +
"" +
DamageTypes.BuildSchema("damage strength") +
Attack.prototype.bonusesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" + // TODO: it shouldn't be stretched
"" +
"" +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
DamageTypes.BuildSchema("damage strength") +
"" + // TODO: how do these work?
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"";
Attack.prototype.Init = function()
{
};
Attack.prototype.Serialize = null; // we have no dynamic state to save
Attack.prototype.GetAttackTypes = function(wantedTypes)
{
let types = g_AttackTypes.filter(type => !!this.template[type]);
if (!wantedTypes)
return types;
let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0);
return types.filter(type => wantedTypes.indexOf("!" + type) == -1 &&
(!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1));
};
Attack.prototype.GetPreferredClasses = function(type)
{
if (this.template[type] && this.template[type].PreferredClasses &&
this.template[type].PreferredClasses._string)
return this.template[type].PreferredClasses._string.split(/\s+/);
return [];
};
Attack.prototype.GetRestrictedClasses = function(type)
{
if (this.template[type] && this.template[type].RestrictedClasses &&
this.template[type].RestrictedClasses._string)
return this.template[type].RestrictedClasses._string.split(/\s+/);
return [];
};
Attack.prototype.CanAttack = function(target, wantedTypes)
{
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
return true;
let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
return false;
let cmpIdentity = QueryMiragedInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
let targetClasses = cmpIdentity.GetClassesList();
if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter &&
(!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length))
return true;
let cmpEntityPlayer = QueryOwnerInterface(this.entity);
let cmpTargetPlayer = QueryOwnerInterface(target);
if (!cmpTargetPlayer || !cmpEntityPlayer)
return false;
let types = this.GetAttackTypes(wantedTypes);
let entityOwner = cmpEntityPlayer.GetPlayerID();
let targetOwner = cmpTargetPlayer.GetPlayerID();
let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
// Check if the relative height difference is larger than the attack range
// If the relative height is bigger, it means they will never be able to
// reach each other, no matter how close they come.
let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
for (let type of types)
{
if (type != "Capture" && !cmpEntityPlayer.IsEnemy(targetOwner))
continue;
if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner)))
continue;
if (heightDiff > this.GetRange(type).max)
continue;
let restrictedClasses = this.GetRestrictedClasses(type);
if (!restrictedClasses.length)
return true;
if (!MatchesClassList(targetClasses, restrictedClasses))
return true;
}
return false;
};
/**
* Returns null if we have no preference or the lowest index of a preferred class.
*/
Attack.prototype.GetPreference = function(target)
{
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
let targetClasses = cmpIdentity.GetClassesList();
let minPref = null;
for (let type of this.GetAttackTypes())
{
let preferredClasses = this.GetPreferredClasses(type);
for (let targetClass of targetClasses)
{
let pref = preferredClasses.indexOf(targetClass);
if (pref === 0)
return pref;
if (pref != -1 && (minPref === null || minPref > pref))
minPref = pref;
}
}
return minPref;
};
/**
* Get the full range of attack using all available attack types.
*/
Attack.prototype.GetFullAttackRange = function()
{
let ret = { "min": Infinity, "max": 0 };
for (let type of this.GetAttackTypes())
{
let range = this.GetRange(type);
ret.min = Math.min(ret.min, range.min);
ret.max = Math.max(ret.max, range.max);
}
return ret;
};
Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
{
// TODO: Formation against formation needs review
let types = this.GetAttackTypes();
return g_AttackTypes.find(attack => types.indexOf(attack) != -1);
}
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
let targetClasses = cmpIdentity.GetClassesList();
let isTargetClass = className => targetClasses.indexOf(className) != -1;
// Always slaughter domestic animals instead of using a normal attack
if (isTargetClass("Domestic") && this.template.Slaughter)
return "Slaughter";
let types = this.GetAttackTypes().filter(type => !this.GetRestrictedClasses(type).some(isTargetClass));
// check if the target is capturable
let captureIndex = types.indexOf("Capture");
if (captureIndex != -1)
{
let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
let cmpPlayer = QueryOwnerInterface(this.entity);
if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
return "Capture";
// not capturable, so remove this attack
types.splice(captureIndex, 1);
}
let isPreferred = className => this.GetPreferredClasses(className).some(isTargetClass);
return types.sort((a, b) =>
(types.indexOf(a) + (isPreferred(a) ? types.length : 0)) -
(types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
};
Attack.prototype.CompareEntitiesByPreference = function(a, b)
{
let aPreference = this.GetPreference(a);
let bPreference = this.GetPreference(b);
if (aPreference === null && bPreference === null) return 0;
if (aPreference === null) return 1;
if (bPreference === null) return -1;
return aPreference - bPreference;
};
Attack.prototype.GetTimers = function(type)
{
let prepare = +(this.template[type].PrepareTime || 0);
prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity);
let repeat = +(this.template[type].RepeatTime || 1000);
repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity);
return { "prepare": prepare, "repeat": repeat };
};
Attack.prototype.GetAttackStrengths = function(type)
{
// Work out the attack values with technology effects
let template = this.template[type];
let splash = "";
if (!template)
{
template = this.template[type.split(".")[0]].Splash;
splash = "/Splash";
}
let applyMods = damageType =>
ApplyValueModificationsToEntity("Attack/" + type + splash + "/" + damageType, +(template[damageType] || 0), this.entity);
if (type == "Capture")
return { "value": applyMods("Value") };
let ret = {};
for (let damageType of DamageTypes.GetTypes())
ret[damageType] = applyMods(damageType);
return ret;
};
Attack.prototype.GetSplashDamage = function(type)
{
if (!this.template[type].Splash)
return false;
let splash = this.GetAttackStrengths(type + ".Splash");
splash.friendlyFire = this.template[type].Splash.FriendlyFire != "false";
splash.shape = this.template[type].Splash.Shape;
return splash;
};
Attack.prototype.GetRange = function(type)
{
let max = +this.template[type].MaxRange;
max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity);
let min = +(this.template[type].MinRange || 0);
min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity);
let elevationBonus = +(this.template[type].ElevationBonus || 0);
elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity);
return { "max": max, "min": min, "elevationBonus": elevationBonus };
};
Attack.prototype.GetBonusTemplate = function(type)
{
let template = this.template[type];
if (!template)
template = this.template[type.split(".")[0]].Splash;
return template.Bonuses || null;
};
/**
* Attack the target entity. This should only be called after a successful range check,
* and should only be called after GetTimers().repeat msec has passed since the last
* call to PerformAttack.
*/
Attack.prototype.PerformAttack = function(type, target)
{
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
// If this is a ranged attack, then launch a projectile
if (type == "Ranged")
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let turnLength = cmpTimer.GetLatestTurnLength()/1000;
// In the future this could be extended:
// * Obstacles like trees could reduce the probability of the target being hit
// * Obstacles like walls should block projectiles entirely
let horizSpeed = +this.template[type].ProjectileSpeed;
let gravity = +this.template[type].Gravity;
//horizSpeed /= 2; gravity /= 2; // slow it down for testing
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
let selfPosition = cmpPosition.GetPosition();
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
let targetPosition = cmpTargetPosition.GetPosition();
let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength);
let timeToTarget = this.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition;
// Add inaccuracy based on spread.
let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template.Ranged.Spread, this.entity) *
predictedPosition.horizDistanceTo(selfPosition) / 100;
let randNorm = randomNormal2D();
let offsetX = randNorm[0] * distanceModifiedSpread;
let offsetZ = randNorm[1] * distanceModifiedSpread;
let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ);
// Recalculate when the missile will hit the target position.
let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition);
timeToTarget = realHorizDistance / horizSpeed;
let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance);
// Launch the graphical projectile.
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
- let id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
+
+ let actorName = "";
+ let impactActorName = "";
+ let impactAnimationLifetime = 0;
+ if (this.template.Ranged.Projectile)
+ {
+ actorName = this.template.Ranged.Projectile.ActorName || "";
+ impactActorName = this.template.Ranged.Projectile.ImpactActorName || "";
+ impactAnimationLifetime = this.template.Ranged.Projectile.ImpactAnimationLifetime || 0;
+ }
+
+ let launchPoint = selfPosition.clone();
+ // TODO: remove this when all the ranged unit templates are updated with Projectile/Launchpoint
+ launchPoint.y += 3;
+
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (cmpVisual)
+ {
+ // if the projectile definition is missing from the template
+ // then fallback to the projectile name and launchpoint in the visual actor
+ if (!actorName)
+ actorName = cmpVisual.GetProjectileActor();
+
+ let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint();
+ if (visualActorLaunchPoint.length() > 0)
+ launchPoint = visualActorLaunchPoint;
+ }
+
+ let id = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, realTargetPosition, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime);
let attackImpactSound = "";
let cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (cmpSound)
attackImpactSound = cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase());
let data = {
"type": type,
"attacker": this.entity,
"target": target,
"strengths": this.GetAttackStrengths(type),
"position": realTargetPosition,
"direction": missileDirection,
"projectileId": id,
"bonus": this.GetBonusTemplate(type),
"isSplash": false,
"attackerOwner": attackerOwner,
"attackImpactSound": attackImpactSound
};
if (this.template.Ranged.Splash)
{
data.friendlyFire = this.template.Ranged.Splash.FriendlyFire != "false";
data.radius = +this.template.Ranged.Splash.Range;
data.shape = this.template.Ranged.Splash.Shape;
data.isSplash = true;
data.splashStrengths = this.GetAttackStrengths(type + ".Splash");
data.splashBonus = this.GetBonusTemplate(type + ".Splash");
}
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000 + +this.template.Ranged.Delay, data);
}
else if (type == "Capture")
{
if (attackerOwner == -1)
return;
let multiplier = GetDamageBonus(target, this.GetBonusTemplate(type));
let cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.GetHitpoints() == 0)
return;
multiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints());
let cmpCapturable = Engine.QueryInterface(target, IID_Capturable);
if (!cmpCapturable || !cmpCapturable.CanCapture(attackerOwner))
return;
let strength = this.GetAttackStrengths("Capture").value * multiplier;
if (cmpCapturable.Reduce(strength, attackerOwner) && IsOwnedByEnemyOfPlayer(attackerOwner, target))
Engine.PostMessage(target, MT_Attacked, {
"attacker": this.entity,
"target": target,
"type": type,
"damage": strength,
"attackerOwner": attackerOwner
});
}
else
{
// Melee attack - hurt the target immediately
cmpDamage.CauseDamage({
"strengths": this.GetAttackStrengths(type),
"target": target,
"attacker": this.entity,
"multiplier": GetDamageBonus(target, this.GetBonusTemplate(type)),
"type": type,
"attackerOwner": attackerOwner
});
}
};
/**
* Get the predicted time of collision between a projectile (or a chaser)
* and its target, assuming they both move in straight line at a constant speed.
* Vertical component of movement is ignored.
* @param {Vector3D} selfPosition - the 3D position of the projectile (or chaser).
* @param {number} horizSpeed - the horizontal speed of the projectile (or chaser).
* @param {Vector3D} targetPosition - the 3D position of the target.
* @param {Vector3D} targetVelocity - the 3D velocity vector of the target.
* @return {Vector3D|boolean} - the 3D predicted position or false if the collision will not happen.
*/
Attack.prototype.PredictTimeToTarget = function(selfPosition, horizSpeed, targetPosition, targetVelocity)
{
let relativePosition = new Vector3D.sub(targetPosition, selfPosition);
let a = targetVelocity.x * targetVelocity.x + targetVelocity.z * targetVelocity.z - horizSpeed * horizSpeed;
let b = relativePosition.x * targetVelocity.x + relativePosition.z * targetVelocity.z;
let c = relativePosition.x * relativePosition.x + relativePosition.z * relativePosition.z;
// The predicted time to reach the target is the smallest non negative solution
// (when it exists) of the equation a t^2 + 2 b t + c = 0.
// Using c>=0, we can straightly compute the right solution.
if (c == 0)
return 0;
let disc = b * b - a * c;
if (a < 0 || b < 0 && disc >= 0)
return c / (Math.sqrt(disc) - b);
return false;
};
Attack.prototype.OnValueModification = function(msg)
{
if (msg.component != "Attack")
return;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (!cmpUnitAI)
return;
if (this.GetAttackTypes().some(type =>
msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1))
cmpUnitAI.UpdateRangeQueries();
};
Attack.prototype.GetRangeOverlays = function()
{
if (!this.template.Ranged || !this.template.Ranged.RangeOverlay)
return [];
let range = this.GetRange("Ranged");
let rangeOverlays = [];
for (let i in range)
if ((i == "min" || i == "max") && range[i])
rangeOverlays.push({
"radius": range[i],
"texture": this.template.Ranged.RangeOverlay.LineTexture,
"textureMask": this.template.Ranged.RangeOverlay.LineTextureMask,
"thickness": +this.template.Ranged.RangeOverlay.LineThickness,
});
return rangeOverlays;
};
Engine.RegisterComponentType(IID_Attack, "Attack", Attack);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml (revision 20675)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml (revision 20676)
@@ -1,78 +1,82 @@
0.0
10.0
100.0
80.0
12.0
37.5
9.81
4000
5000
4.0
0
+
+ props/units/weapons/rock_explosion.xml
+ 0.1
+
Circular
10
false
0.0
15.0
35.0
Structure
20
25
400
250
4.5
250
Siege Catapult
Catapult Ranged
300
0
20
10
0
square/256x256.png
square/256x256_mask.png
attack/impact/siegeprojectilehit.xml
attack/siege/ballist_attack.xml
0.8
0.8
120
Index: ps/trunk/source/simulation2/components/CCmpProjectileManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpProjectileManager.cpp (revision 20675)
+++ ps/trunk/source/simulation2/components/CCmpProjectileManager.cpp (revision 20676)
@@ -1,345 +1,396 @@
/* 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 "ICmpProjectileManager.h"
#include "ICmpObstruction.h"
#include "ICmpObstructionManager.h"
#include "ICmpPosition.h"
#include "ICmpRangeManager.h"
#include "ICmpTerrain.h"
-#include "ICmpVisual.h"
#include "simulation2/MessageTypes.h"
#include "graphics/Frustum.h"
#include "graphics/Model.h"
#include "graphics/Unit.h"
#include "graphics/UnitManager.h"
#include "maths/Matrix3D.h"
#include "maths/Quaternion.h"
#include "maths/Vector3D.h"
#include "ps/CLogger.h"
#include "renderer/Scene.h"
// Time (in seconds) before projectiles that stuck in the ground are destroyed
const static float PROJECTILE_DECAY_TIME = 30.f;
class CCmpProjectileManager : public ICmpProjectileManager
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Interpolate);
componentManager.SubscribeToMessageType(MT_RenderSubmit);
}
DEFAULT_COMPONENT_ALLOCATOR(ProjectileManager)
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_ActorSeed = 0;
m_NextId = 1;
}
virtual void Deinit()
{
for (size_t i = 0; i < m_Projectiles.size(); ++i)
GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles[i].unit);
m_Projectiles.clear();
}
virtual void Serialize(ISerializer& serialize)
{
// Because this is just graphical effects, and because it's all non-deterministic floating point,
// we don't do much serialization here.
// (That means projectiles will vanish if you save/load - is that okay?)
// The attack code stores the id so that the projectile gets deleted when it hits the target
serialize.NumberU32_Unbounded("next id", m_NextId);
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
// The attack code stores the id so that the projectile gets deleted when it hits the target
deserialize.NumberU32_Unbounded("next id", m_NextId);
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Interpolate:
{
const CMessageInterpolate& msgData = static_cast (msg);
Interpolate(msgData.deltaSimTime);
break;
}
case MT_RenderSubmit:
{
const CMessageRenderSubmit& msgData = static_cast (msg);
RenderSubmit(msgData.collector, msgData.frustum, msgData.culling);
break;
}
}
}
- virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, const CFixedVector3D& target, fixed speed, fixed gravity)
+ virtual uint32_t LaunchProjectileAtPoint(const CFixedVector3D& launchPoint, const CFixedVector3D& target, fixed speed, fixed gravity, const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime)
{
- return LaunchProjectile(source, target, speed, gravity);
+ return LaunchProjectile(launchPoint, target, speed, gravity, actorName, impactActorName, impactAnimationLifetime);
}
virtual void RemoveProjectile(uint32_t);
+ void RenderModel(CModelAbstract& model, const CVector3D& position, SceneCollector& collector, const CFrustum& frustum, bool culling,
+ ICmpRangeManager::CLosQuerier los, bool losRevealAll) const;
+
private:
struct Projectile
{
CUnit* unit;
CVector3D origin;
CVector3D pos;
CVector3D v;
float time;
float timeHit;
float gravity;
- bool stopped;
+ float impactAnimationLifetime;
uint32_t id;
+ std::wstring impactActorName;
+ bool isImpactAnimationCreated;
+ bool stopped;
CVector3D position(float t)
{
float t2 = t;
if (t2 > timeHit)
t2 = timeHit + logf(1.f + t2 - timeHit);
CVector3D ret(origin);
ret.X += v.X * t2;
ret.Z += v.Z * t2;
ret.Y += v.Y * t2 - 0.5f * gravity * t * t;
return ret;
}
};
+ struct ProjectileImpactAnimation
+ {
+ CUnit* unit;
+ CVector3D pos;
+ float time;
+ };
+
std::vector m_Projectiles;
+ std::vector m_ProjectileImpactAnimations;
+
uint32_t m_ActorSeed;
uint32_t m_NextId;
- uint32_t LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity);
+ uint32_t LaunchProjectile(CFixedVector3D launchPoint, CFixedVector3D targetPoint, fixed speed, fixed gravity,
+ const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime);
void AdvanceProjectile(Projectile& projectile, float dt) const;
void Interpolate(float frameTime);
void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) const;
};
REGISTER_COMPONENT_TYPE(ProjectileManager)
-uint32_t CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity)
+uint32_t CCmpProjectileManager::LaunchProjectile(CFixedVector3D launchPoint, CFixedVector3D targetPoint, fixed speed, fixed gravity, const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime)
{
// This is network synced so don't use GUI checks before incrementing or it breaks any non GUI simulations
uint32_t currentId = m_NextId++;
- if (!GetSimContext().HasUnitManager())
+ if (!GetSimContext().HasUnitManager() || actorName.empty())
return currentId; // do nothing if graphics are disabled
- CmpPtr cmpSourceVisual(GetSimContext(), source);
- if (!cmpSourceVisual)
- return currentId;
-
- std::wstring name = cmpSourceVisual->GetProjectileActor();
- if (name.empty())
- {
- // If the actor was actually loaded, complain that it doesn't have a projectile
- if (!cmpSourceVisual->GetActorShortName().empty())
- LOGERROR("Unit with actor '%s' launched a projectile but has no actor on 'projectile' attachpoint", utf8_from_wstring(cmpSourceVisual->GetActorShortName()));
- return currentId;
- }
-
Projectile projectile;
projectile.id = currentId;
projectile.time = 0.f;
projectile.stopped = false;
projectile.gravity = gravity.ToFloat();
+ projectile.isImpactAnimationCreated = false;
- projectile.origin = cmpSourceVisual->GetProjectileLaunchPoint();
- if (!projectile.origin)
+ if (!impactActorName.empty())
+ {
+ projectile.impactActorName = impactActorName;
+ projectile.impactAnimationLifetime = impactAnimationLifetime.ToFloat();
+ }
+ else
{
- // If there's no explicit launch point, take a guess based on the entity position
- CmpPtr sourcePos(GetSimContext(), source);
- if (!sourcePos)
- return currentId;
- projectile.origin = sourcePos->GetPosition();
- projectile.origin.Y += 3.f;
+ projectile.impactActorName = L"";
+ projectile.impactAnimationLifetime = 0.0f;
}
+ projectile.origin = launchPoint;
+
std::set selections;
- projectile.unit = GetSimContext().GetUnitManager().CreateUnit(name, m_ActorSeed++, selections);
+ projectile.unit = GetSimContext().GetUnitManager().CreateUnit(actorName, m_ActorSeed++, selections);
if (!projectile.unit) // The error will have already been logged
return currentId;
projectile.pos = projectile.origin;
CVector3D offset(targetPoint);
offset -= projectile.pos;
float horizDistance = sqrtf(offset.X*offset.X + offset.Z*offset.Z);
projectile.timeHit = horizDistance / speed.ToFloat();
projectile.v = offset * (1.f / projectile.timeHit);
projectile.v.Y = offset.Y / projectile.timeHit + 0.5f * projectile.gravity * projectile.timeHit;
m_Projectiles.push_back(projectile);
return projectile.id;
}
void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt) const
{
projectile.time += dt;
if (projectile.stopped)
return;
CVector3D delta;
if (dt < 0.1f)
delta = projectile.pos;
else // For big dt delta is unprecise
delta = projectile.position(projectile.time - 0.1f);
projectile.pos = projectile.position(projectile.time);
delta = projectile.pos - delta;
// If we've passed the target position and haven't stopped yet,
// carry on until we reach solid land
if (projectile.time >= projectile.timeHit)
{
CmpPtr cmpTerrain(GetSystemEntity());
if (cmpTerrain)
{
float h = cmpTerrain->GetExactGroundLevel(projectile.pos.X, projectile.pos.Z);
if (projectile.pos.Y < h)
{
projectile.pos.Y = h; // stick precisely to the terrain
projectile.stopped = true;
}
}
}
// Construct a rotation matrix so that (0,1,0) is in the direction of 'delta'
CVector3D up(0, 1, 0);
delta.Normalize();
CVector3D axis = up.Cross(delta);
if (axis.LengthSquared() < 0.0001f)
axis = CVector3D(1, 0, 0); // if up & delta are almost collinear, rotate around some other arbitrary axis
else
axis.Normalize();
float angle = acosf(up.Dot(delta));
CMatrix3D transform;
CQuaternion quat;
quat.FromAxisAngle(axis, angle);
quat.ToMatrix(transform);
// Then apply the translation
transform.Translate(projectile.pos);
// Move the model
projectile.unit->GetModel().SetTransform(transform);
}
void CCmpProjectileManager::Interpolate(float frameTime)
{
for (size_t i = 0; i < m_Projectiles.size(); ++i)
{
AdvanceProjectile(m_Projectiles[i], frameTime);
}
// Remove the ones that have reached their target
for (size_t i = 0; i < m_Projectiles.size(); )
{
+ if (!m_Projectiles[i].stopped)
+ {
+ ++i;
+ continue;
+ }
+
+ if (!m_Projectiles[i].impactActorName.empty() && !m_Projectiles[i].isImpactAnimationCreated)
+ {
+ m_Projectiles[i].isImpactAnimationCreated = true;
+ CMatrix3D transform;
+ CQuaternion quat;
+ quat.ToMatrix(transform);
+ transform.Translate(m_Projectiles[i].pos);
+
+ std::set selections;
+ CUnit* unit = GetSimContext().GetUnitManager().CreateUnit(m_Projectiles[i].impactActorName, m_ActorSeed++, selections);
+ unit->GetModel().SetTransform(transform);
+
+ ProjectileImpactAnimation projectileImpactAnimation;
+ projectileImpactAnimation.unit = unit;
+ projectileImpactAnimation.time = m_Projectiles[i].impactAnimationLifetime;
+ projectileImpactAnimation.pos = m_Projectiles[i].pos;
+ m_ProjectileImpactAnimations.push_back(projectileImpactAnimation);
+ }
+
// Projectiles hitting targets get removed immediately.
// Those hitting the ground stay for a while, because it looks pretty.
- if (m_Projectiles[i].stopped)
+ if (m_Projectiles[i].time - m_Projectiles[i].timeHit > PROJECTILE_DECAY_TIME)
{
- if (m_Projectiles[i].time - m_Projectiles[i].timeHit > PROJECTILE_DECAY_TIME)
- {
- // Delete in-place by swapping with the last in the list
- std::swap(m_Projectiles[i], m_Projectiles.back());
- GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
- m_Projectiles.pop_back();
- continue; // don't increment i
- }
+ // Delete in-place by swapping with the last in the list
+ std::swap(m_Projectiles[i], m_Projectiles.back());
+ GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
+ m_Projectiles.pop_back();
+ continue;
}
-
++i;
}
+
+ for (size_t i = 0; i < m_ProjectileImpactAnimations.size();)
+ {
+ if (m_ProjectileImpactAnimations[i].time > 0)
+ {
+ m_ProjectileImpactAnimations[i].time -= frameTime;
+ ++i;
+ }
+ else
+ {
+ std::swap(m_ProjectileImpactAnimations[i], m_ProjectileImpactAnimations.back());
+ GetSimContext().GetUnitManager().DeleteUnit(m_ProjectileImpactAnimations.back().unit);
+ m_ProjectileImpactAnimations.pop_back();
+ }
+ }
}
void CCmpProjectileManager::RemoveProjectile(uint32_t id)
{
// Scan through the projectile list looking for one with the correct id to remove
for (size_t i = 0; i < m_Projectiles.size(); i++)
{
if (m_Projectiles[i].id == id)
{
// Delete in-place by swapping with the last in the list
std::swap(m_Projectiles[i], m_Projectiles.back());
GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
m_Projectiles.pop_back();
return;
}
}
}
+void CCmpProjectileManager::RenderModel(CModelAbstract& model, const CVector3D& position, SceneCollector& collector,
+ const CFrustum& frustum, bool culling, ICmpRangeManager::CLosQuerier los, bool losRevealAll) const
+{
+ // Don't display objects outside the visible area
+ ssize_t posi = (ssize_t)(0.5f + position.X / TERRAIN_TILE_SIZE);
+ ssize_t posj = (ssize_t)(0.5f + position.Z / TERRAIN_TILE_SIZE);
+ if (!losRevealAll && !los.IsVisible(posi, posj))
+ return;
+
+ model.ValidatePosition();
+
+ if (culling && !frustum.IsBoxVisible(model.GetWorldBoundsRec()))
+ return;
+
+ // TODO: do something about LOS (copy from CCmpVisualActor)
+
+ collector.SubmitRecursive(&model);
+}
+
void CCmpProjectileManager::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) const
{
CmpPtr cmpRangeManager(GetSystemEntity());
int player = GetSimContext().GetCurrentDisplayedPlayer();
ICmpRangeManager::CLosQuerier los(cmpRangeManager->GetLosQuerier(player));
bool losRevealAll = cmpRangeManager->GetLosRevealAll(player);
- for (size_t i = 0; i < m_Projectiles.size(); ++i)
+ for (const Projectile& projectile : m_Projectiles)
{
- // Don't display projectiles outside the visible area
- ssize_t posi = (ssize_t)(0.5f + m_Projectiles[i].pos.X / TERRAIN_TILE_SIZE);
- ssize_t posj = (ssize_t)(0.5f + m_Projectiles[i].pos.Z / TERRAIN_TILE_SIZE);
- if (!losRevealAll && !los.IsVisible(posi, posj))
- continue;
-
- CModelAbstract& model = m_Projectiles[i].unit->GetModel();
-
- model.ValidatePosition();
-
- if (culling && !frustum.IsBoxVisible(model.GetWorldBoundsRec()))
- continue;
-
- // TODO: do something about LOS (copy from CCmpVisualActor)
+ RenderModel(projectile.unit->GetModel(), projectile.pos, collector, frustum, culling, los, losRevealAll);
+ }
- collector.SubmitRecursive(&model);
+ for (const ProjectileImpactAnimation& projectileImpactAnimation : m_ProjectileImpactAnimations)
+ {
+ RenderModel(projectileImpactAnimation.unit->GetModel(), projectileImpactAnimation.pos,
+ collector, frustum, culling, los, losRevealAll);
}
}
Index: ps/trunk/source/simulation2/components/CCmpVisualActor.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpVisualActor.cpp (revision 20675)
+++ ps/trunk/source/simulation2/components/CCmpVisualActor.cpp (revision 20676)
@@ -1,790 +1,793 @@
/* 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 "ICmpVisual.h"
#include "simulation2/MessageTypes.h"
#include "ICmpFootprint.h"
#include "ICmpUnitRenderer.h"
#include "ICmpOwnership.h"
#include "ICmpPosition.h"
#include "ICmpTemplateManager.h"
#include "ICmpTerrain.h"
#include "ICmpUnitMotion.h"
#include "ICmpValueModificationManager.h"
#include "ICmpVisibility.h"
#include "simulation2/serialization/SerializeTemplates.h"
#include "graphics/Decal.h"
#include "graphics/Frustum.h"
#include "graphics/Model.h"
#include "graphics/ObjectBase.h"
#include "graphics/ObjectEntry.h"
#include "graphics/Unit.h"
#include "graphics/UnitAnimation.h"
#include "graphics/UnitManager.h"
#include "maths/BoundingSphere.h"
#include "maths/Matrix3D.h"
#include "maths/Vector3D.h"
#include "ps/CLogger.h"
#include "ps/GameSetup/Config.h"
#include "renderer/Scene.h"
class CCmpVisualActor : public ICmpVisual
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Update_Final);
componentManager.SubscribeToMessageType(MT_InterpolatedPositionChanged);
componentManager.SubscribeToMessageType(MT_OwnershipChanged);
componentManager.SubscribeToMessageType(MT_ValueModification);
componentManager.SubscribeToMessageType(MT_TerrainChanged);
componentManager.SubscribeToMessageType(MT_Destroy);
}
DEFAULT_COMPONENT_ALLOCATOR(VisualActor)
private:
std::wstring m_BaseActorName, m_ActorName;
bool m_IsFoundationActor;
CUnit* m_Unit;
fixed m_R, m_G, m_B; // shading color
std::map m_AnimOverride;
// Current animation state
fixed m_AnimRunThreshold; // if non-zero this is the special walk/run mode
std::string m_AnimName;
bool m_AnimOnce;
fixed m_AnimSpeed;
std::wstring m_SoundGroup;
fixed m_AnimDesync;
fixed m_AnimSyncRepeatTime; // 0.0 if not synced
fixed m_AnimSyncOffsetTime;
std::map m_VariantSelections;
u32 m_Seed; // seed used for random variations
bool m_ConstructionPreview;
bool m_VisibleInAtlasOnly;
bool m_IsActorOnly; // an in-world entity should not have this or it might not be rendered.
ICmpUnitRenderer::tag_t m_ModelTag;
public:
static std::string GetSchema()
{
return
"Display the unit using the engine's actor system."
""
"units/hellenes/infantry_spearman_b.xml"
""
""
"structures/hellenes/barracks.xml"
"structures/fndn_4x4.xml"
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
"0.0"
""
""
""
""
"0.0"
""
""
""
""
"0.0"
""
""
""
""
""
""
"0.0"
""
""
""
""
"0.0"
""
""
""
""
""
""
""
""
"";
}
virtual void Init(const CParamNode& paramNode)
{
m_Unit = NULL;
m_R = m_G = m_B = fixed::FromInt(1);
m_ConstructionPreview = paramNode.GetChild("ConstructionPreview").IsOk();
m_Seed = GetEntityId();
m_IsFoundationActor = paramNode.GetChild("Foundation").IsOk() && paramNode.GetChild("FoundationActor").IsOk();
if (m_IsFoundationActor)
m_BaseActorName = m_ActorName = paramNode.GetChild("FoundationActor").ToString();
else
m_BaseActorName = m_ActorName = paramNode.GetChild("Actor").ToString();
m_VisibleInAtlasOnly = paramNode.GetChild("VisibleInAtlasOnly").ToBool();
m_IsActorOnly = paramNode.GetChild("ActorOnly").IsOk();
InitModel(paramNode);
SelectAnimation("idle", false, fixed::FromInt(1), L"");
}
virtual void Deinit()
{
if (m_Unit)
{
GetSimContext().GetUnitManager().DeleteUnit(m_Unit);
m_Unit = NULL;
}
}
template
void SerializeCommon(S& serialize)
{
serialize.NumberFixed_Unbounded("r", m_R);
serialize.NumberFixed_Unbounded("g", m_G);
serialize.NumberFixed_Unbounded("b", m_B);
SerializeMap()(serialize, "anim overrides", m_AnimOverride);
serialize.NumberFixed_Unbounded("anim run threshold", m_AnimRunThreshold);
serialize.StringASCII("anim name", m_AnimName, 0, 256);
serialize.Bool("anim once", m_AnimOnce);
serialize.NumberFixed_Unbounded("anim speed", m_AnimSpeed);
serialize.String("sound group", m_SoundGroup, 0, 256);
serialize.NumberFixed_Unbounded("anim desync", m_AnimDesync);
serialize.NumberFixed_Unbounded("anim sync repeat time", m_AnimSyncRepeatTime);
serialize.NumberFixed_Unbounded("anim sync offset time", m_AnimSyncOffsetTime);
SerializeMap()(serialize, "variation", m_VariantSelections);
serialize.NumberU32_Unbounded("seed", m_Seed);
serialize.String("actor", m_ActorName, 0, 256);
// TODO: store actor variables?
}
virtual void Serialize(ISerializer& serialize)
{
// TODO: store the actor name, if !debug and it differs from the template
if (serialize.IsDebug())
{
serialize.String("base actor", m_BaseActorName, 0, 256);
}
SerializeCommon(serialize);
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
u32 oldSeed = GetActorSeed();
SerializeCommon(deserialize);
// If we serialized a different seed or different actor, reload actor
if (oldSeed != GetActorSeed() || m_BaseActorName != m_ActorName)
ReloadActor();
else
ReloadUnitAnimation();
if (m_Unit)
{
CmpPtr cmpOwnership(GetEntityHandle());
if (cmpOwnership)
m_Unit->GetModel().SetPlayerID(cmpOwnership->GetOwner());
}
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Update_Final:
{
const CMessageUpdate_Final& msgData = static_cast (msg);
Update(msgData.turnLength);
break;
}
case MT_OwnershipChanged:
{
if (!m_Unit)
break;
const CMessageOwnershipChanged& msgData = static_cast (msg);
m_Unit->GetModel().SetPlayerID(msgData.to);
break;
}
case MT_TerrainChanged:
{
if (!m_Unit)
break;
const CMessageTerrainChanged& msgData = static_cast (msg);
m_Unit->GetModel().SetTerrainDirty(msgData.i0, msgData.j0, msgData.i1, msgData.j1);
break;
}
case MT_ValueModification:
{
const CMessageValueModification& msgData = static_cast (msg);
if (msgData.component != L"VisualActor")
break;
CmpPtr cmpValueModificationManager(GetSystemEntity());
std::wstring newActorName;
if (m_IsFoundationActor)
newActorName = cmpValueModificationManager->ApplyModifications(L"VisualActor/FoundationActor", m_BaseActorName, GetEntityId());
else
newActorName = cmpValueModificationManager->ApplyModifications(L"VisualActor/Actor", m_BaseActorName, GetEntityId());
if (newActorName != m_ActorName)
{
m_ActorName = newActorName;
ReloadActor();
}
break;
}
case MT_InterpolatedPositionChanged:
{
const CMessageInterpolatedPositionChanged& msgData = static_cast (msg);
if (m_ModelTag.valid())
{
CmpPtr cmpModelRenderer(GetSystemEntity());
cmpModelRenderer->UpdateUnitPos(m_ModelTag, msgData.inWorld, msgData.pos0, msgData.pos1);
}
break;
}
case MT_Destroy:
{
if (m_ModelTag.valid())
{
CmpPtr cmpModelRenderer(GetSystemEntity());
cmpModelRenderer->RemoveUnit(m_ModelTag);
m_ModelTag = ICmpUnitRenderer::tag_t();
}
break;
}
}
}
virtual CBoundingBoxAligned GetBounds() const
{
if (!m_Unit)
return CBoundingBoxAligned::EMPTY;
return m_Unit->GetModel().GetWorldBounds();
}
virtual CUnit* GetUnit()
{
return m_Unit;
}
virtual CBoundingBoxOriented GetSelectionBox() const
{
if (!m_Unit)
return CBoundingBoxOriented::EMPTY;
return m_Unit->GetModel().GetSelectionBox();
}
virtual CVector3D GetPosition() const
{
if (!m_Unit)
return CVector3D(0, 0, 0);
return m_Unit->GetModel().GetTransform().GetTranslation();
}
virtual std::wstring GetActorShortName() const
{
if (!m_Unit)
return L"";
return m_Unit->GetObject().m_Base->m_ShortName;
}
virtual std::wstring GetProjectileActor() const
{
if (!m_Unit)
return L"";
return m_Unit->GetObject().m_ProjectileModelName;
}
- virtual CVector3D GetProjectileLaunchPoint() const
+ virtual CFixedVector3D GetProjectileLaunchPoint() const
{
if (!m_Unit)
- return CVector3D();
+ return CFixedVector3D();
if (m_Unit->GetModel().ToCModel())
{
// Ensure the prop transforms are correct
CmpPtr cmpUnitRenderer(GetSystemEntity());
CmpPtr cmpPosition(GetEntityHandle());
if (cmpUnitRenderer && cmpPosition)
{
float frameOffset = cmpUnitRenderer->GetFrameOffset();
CMatrix3D transform(cmpPosition->GetInterpolatedTransform(frameOffset));
m_Unit->GetModel().SetTransform(transform);
m_Unit->GetModel().ValidatePosition();
}
CModelAbstract* ammo = m_Unit->GetModel().ToCModel()->FindFirstAmmoProp();
if (ammo)
- return ammo->GetTransform().GetTranslation();
+ {
+ CVector3D vector = ammo->GetTransform().GetTranslation();
+ return CFixedVector3D(fixed::FromFloat(vector.X), fixed::FromFloat(vector.Y), fixed::FromFloat(vector.Z));
+ }
}
- return CVector3D();
+ return CFixedVector3D();
}
virtual void SetVariant(const CStr& key, const CStr& selection)
{
m_VariantSelections[key] = selection;
if (m_Unit)
m_Unit->SetEntitySelection(key, selection);
}
virtual std::string GetAnimationName() const
{
return m_AnimName;
}
virtual void SelectAnimation(const std::string& name, bool once, fixed speed, const std::wstring& soundgroup)
{
m_AnimRunThreshold = fixed::Zero();
m_AnimName = name;
m_AnimOnce = once;
m_AnimSpeed = speed;
m_SoundGroup = soundgroup;
m_AnimDesync = fixed::FromInt(1)/20; // TODO: make this an argument
m_AnimSyncRepeatTime = fixed::Zero();
m_AnimSyncOffsetTime = fixed::Zero();
SetVariant("animation", m_AnimName);
if (m_Unit && m_Unit->GetAnimation())
m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str());
}
virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace)
{
m_AnimOverride[name] = replace;
}
virtual void ResetMoveAnimation(const std::string& name)
{
std::map::const_iterator it = m_AnimOverride.find(name);
if (it != m_AnimOverride.end())
m_AnimOverride.erase(name);
}
virtual void SelectMovementAnimation(fixed runThreshold)
{
SelectAnimation("walk", false, fixed::FromFloat(1.f), L"");
m_AnimRunThreshold = runThreshold;
}
virtual void SetAnimationSyncRepeat(fixed repeattime)
{
m_AnimSyncRepeatTime = repeattime;
if (m_Unit && m_Unit->GetAnimation())
m_Unit->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime.ToFloat());
}
virtual void SetAnimationSyncOffset(fixed actiontime)
{
m_AnimSyncOffsetTime = actiontime;
if (m_Unit && m_Unit->GetAnimation())
m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat());
}
virtual void SetShadingColor(fixed r, fixed g, fixed b, fixed a)
{
m_R = r;
m_G = g;
m_B = b;
UNUSED2(a); // TODO: why is this even an argument?
if (m_Unit)
{
CModelAbstract& model = m_Unit->GetModel();
model.SetShadingColor(CColor(m_R.ToFloat(), m_G.ToFloat(), m_B.ToFloat(), 1.0f));
}
}
virtual void SetVariable(const std::string& name, float value)
{
if (m_Unit)
m_Unit->GetModel().SetEntityVariable(name, value);
}
virtual u32 GetActorSeed() const
{
return m_Seed;
}
virtual void SetActorSeed(u32 seed)
{
if (seed == m_Seed)
return;
m_Seed = seed;
ReloadActor();
}
virtual bool HasConstructionPreview() const
{
return m_ConstructionPreview;
}
virtual void Hotload(const VfsPath& name)
{
if (!m_Unit)
return;
if (name != m_ActorName)
return;
ReloadActor();
}
private:
/// Helper function shared by component init and actor reloading
void InitModel(const CParamNode& paramNode);
/// Helper method; initializes the model selection shape descriptor from XML. Factored out for readability of @ref Init.
void InitSelectionShapeDescriptor(const CParamNode& paramNode);
// ReloadActor is used when the actor or seed changes.
void ReloadActor();
// ReloadUnitAnimation is used for a minimal reloading upon deserialization, when the actor and seed are identical.
// It is also used by ReloadActor.
void ReloadUnitAnimation();
void Update(fixed turnLength);
};
REGISTER_COMPONENT_TYPE(VisualActor)
// ------------------------------------------------------------------------------------------------------------------
void CCmpVisualActor::InitModel(const CParamNode& paramNode)
{
if (!GetSimContext().HasUnitManager())
return;
std::set selections;
std::wstring actorName = m_ActorName;
if (actorName.find(L".xml") == std::wstring::npos)
actorName += L".xml";
m_Unit = GetSimContext().GetUnitManager().CreateUnit(actorName, GetActorSeed(), selections);
if (!m_Unit)
return;
CModelAbstract& model = m_Unit->GetModel();
if (model.ToCModel())
{
u32 modelFlags = 0;
if (paramNode.GetChild("SilhouetteDisplay").ToBool())
modelFlags |= MODELFLAG_SILHOUETTE_DISPLAY;
if (paramNode.GetChild("SilhouetteOccluder").ToBool())
modelFlags |= MODELFLAG_SILHOUETTE_OCCLUDER;
CmpPtr cmpVisibility(GetEntityHandle());
if (cmpVisibility && cmpVisibility->GetAlwaysVisible())
modelFlags |= MODELFLAG_IGNORE_LOS;
model.ToCModel()->AddFlagsRec(modelFlags);
}
if (paramNode.GetChild("DisableShadows").IsOk())
{
if (model.ToCModel())
model.ToCModel()->RemoveShadowsRec();
else if (model.ToCModelDecal())
model.ToCModelDecal()->RemoveShadows();
}
// Initialize the model's selection shape descriptor. This currently relies on the component initialization order; the
// Footprint component must be initialized before this component (VisualActor) to support the ability to use the footprint
// shape for the selection box (instead of the default recursive bounding box). See TypeList.h for the order in
// which components are initialized; if for whatever reason you need to get rid of this dependency, you can always just
// initialize the selection shape descriptor on-demand.
InitSelectionShapeDescriptor(paramNode);
m_Unit->SetID(GetEntityId());
bool floating = m_Unit->GetObject().m_Base->m_Properties.m_FloatOnWater;
CmpPtr cmpPosition(GetEntityHandle());
if (cmpPosition)
cmpPosition->SetActorFloating(floating);
if (!m_ModelTag.valid())
{
CmpPtr cmpModelRenderer(GetSystemEntity());
if (cmpModelRenderer)
{
// TODO: this should account for all possible props, animations, etc,
// else we might accidentally cull the unit when it should be visible
CBoundingBoxAligned bounds = m_Unit->GetModel().GetWorldBoundsRec();
CBoundingSphere boundSphere = CBoundingSphere::FromSweptBox(bounds);
int flags = 0;
if (m_IsActorOnly)
flags |= ICmpUnitRenderer::ACTOR_ONLY;
if (m_VisibleInAtlasOnly)
flags |= ICmpUnitRenderer::VISIBLE_IN_ATLAS_ONLY;
m_ModelTag = cmpModelRenderer->AddUnit(GetEntityHandle(), m_Unit, boundSphere, flags);
}
}
}
void CCmpVisualActor::InitSelectionShapeDescriptor(const CParamNode& paramNode)
{
// by default, we don't need a custom selection shape and we can just keep the default behaviour
CModelAbstract::CustomSelectionShape* shapeDescriptor = NULL;
const CParamNode& shapeNode = paramNode.GetChild("SelectionShape");
if (shapeNode.IsOk())
{
if (shapeNode.GetChild("Bounds").IsOk())
{
// default; no need to take action
}
else if (shapeNode.GetChild("Footprint").IsOk())
{
CmpPtr cmpFootprint(GetEntityHandle());
if (cmpFootprint)
{
ICmpFootprint::EShape fpShape; // fp stands for "footprint"
entity_pos_t fpSize0, fpSize1, fpHeight; // fp stands for "footprint"
cmpFootprint->GetShape(fpShape, fpSize0, fpSize1, fpHeight);
float size0 = fpSize0.ToFloat();
float size1 = fpSize1.ToFloat();
// TODO: we should properly distinguish between CIRCLE and SQUARE footprint shapes here, but since cylinders
// aren't implemented yet and are almost indistinguishable from boxes for small enough sizes anyway,
// we'll just use boxes for either case. However, for circular footprints the size0 and size1 values both
// represent the radius, so we do have to adjust them to match the size1 and size0's of square footprints
// (which represent the full width and depth).
if (fpShape == ICmpFootprint::CIRCLE)
{
size0 *= 2;
size1 *= 2;
}
shapeDescriptor = new CModelAbstract::CustomSelectionShape;
shapeDescriptor->m_Type = CModelAbstract::CustomSelectionShape::BOX;
shapeDescriptor->m_Size0 = size0;
shapeDescriptor->m_Size1 = size1;
shapeDescriptor->m_Height = fpHeight.ToFloat();
}
else
{
LOGERROR("[VisualActor] Cannot apply footprint-based SelectionShape; Footprint component not initialized.");
}
}
else if (shapeNode.GetChild("Box").IsOk())
{
// TODO: we might need to support the ability to specify a different box center in the future
shapeDescriptor = new CModelAbstract::CustomSelectionShape;
shapeDescriptor->m_Type = CModelAbstract::CustomSelectionShape::BOX;
shapeDescriptor->m_Size0 = shapeNode.GetChild("Box").GetChild("@width").ToFixed().ToFloat();
shapeDescriptor->m_Size1 = shapeNode.GetChild("Box").GetChild("@depth").ToFixed().ToFloat();
shapeDescriptor->m_Height = shapeNode.GetChild("Box").GetChild("@height").ToFixed().ToFloat();
}
else if (shapeNode.GetChild("Cylinder").IsOk())
{
LOGWARNING("[VisualActor] TODO: Cylinder selection shapes are not yet implemented; defaulting to recursive bounding boxes");
}
else
{
// shouldn't happen by virtue of validation against schema
LOGERROR("[VisualActor] No selection shape specified");
}
}
ENSURE(m_Unit);
// the model is now responsible for cleaning up the descriptor
m_Unit->GetModel().SetCustomSelectionShape(shapeDescriptor);
}
void CCmpVisualActor::ReloadActor()
{
if (!m_Unit)
return;
// Save some data from the old unit
CColor shading = m_Unit->GetModel().GetShadingColor();
player_id_t playerID = m_Unit->GetModel().GetPlayerID();
// Replace with the new unit
GetSimContext().GetUnitManager().DeleteUnit(m_Unit);
// HACK: selection shape needs template data, but rather than storing all that data
// in the component, we load the template here and pass it into a helper function
CmpPtr cmpTemplateManager(GetSystemEntity());
const CParamNode* node = cmpTemplateManager->LoadLatestTemplate(GetEntityId());
ENSURE(node && node->GetChild("VisualActor").IsOk());
InitModel(node->GetChild("VisualActor"));
ReloadUnitAnimation();
m_Unit->GetModel().SetShadingColor(shading);
m_Unit->GetModel().SetPlayerID(playerID);
if (m_ModelTag.valid())
{
CmpPtr cmpModelRenderer(GetSystemEntity());
CBoundingBoxAligned bounds = m_Unit->GetModel().GetWorldBoundsRec();
CBoundingSphere boundSphere = CBoundingSphere::FromSweptBox(bounds);
cmpModelRenderer->UpdateUnit(m_ModelTag, m_Unit, boundSphere);
}
}
void CCmpVisualActor::ReloadUnitAnimation()
{
if (!m_Unit)
return;
m_Unit->SetEntitySelection(m_VariantSelections);
if (!m_Unit->GetAnimation())
return;
m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str());
// We'll lose the exact synchronisation but we should at least make sure it's going at the correct rate
if (!m_AnimSyncRepeatTime.IsZero())
m_Unit->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime.ToFloat());
if (!m_AnimSyncOffsetTime.IsZero())
m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat());
}
void CCmpVisualActor::Update(fixed UNUSED(turnLength))
{
// This function is currently only used to update the animation if the speed in
// CCmpUnitMotion changes. This also only happens in the "special movement mode"
// triggered by SelectMovementAnimation.
// TODO: This should become event based, in order to save performance and to make the code
// far less hacky. We should also take into account the speed when the animation is different
// from the "special movement mode" walking animation.
// If we're not in the special movement mode, nothing to do.
if (m_AnimRunThreshold.IsZero())
return;
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return;
CmpPtr cmpUnitMotion(GetEntityHandle());
if (!cmpUnitMotion)
return;
fixed speed = cmpUnitMotion->GetCurrentSpeed();
std::string name;
if (speed.IsZero())
{
speed = fixed::FromFloat(1.f);
name = "idle";
}
else
name = speed < m_AnimRunThreshold ? "walk" : "run";
std::map::const_iterator it = m_AnimOverride.find(name);
if (it != m_AnimOverride.end())
name = it->second;
// Selecting the animation is going to reset the anim run threshold, so save it
fixed runThreshold = m_AnimRunThreshold;
SelectAnimation(name, false, speed, L"");
m_AnimRunThreshold = runThreshold;
}
Index: ps/trunk/source/simulation2/components/ICmpProjectileManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpProjectileManager.cpp (revision 20675)
+++ ps/trunk/source/simulation2/components/ICmpProjectileManager.cpp (revision 20676)
@@ -1,27 +1,27 @@
/* Copyright (C) 2010 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 "ICmpProjectileManager.h"
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(ProjectileManager)
-DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", uint32_t, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed)
+DEFINE_INTERFACE_METHOD_7("LaunchProjectileAtPoint", uint32_t, ICmpProjectileManager, LaunchProjectileAtPoint, CFixedVector3D, CFixedVector3D, fixed, fixed, std::wstring, std::wstring, fixed)
DEFINE_INTERFACE_METHOD_1("RemoveProjectile", void, ICmpProjectileManager, RemoveProjectile, uint32_t)
END_INTERFACE_WRAPPER(ProjectileManager)
Index: ps/trunk/source/simulation2/components/ICmpProjectileManager.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpProjectileManager.h (revision 20675)
+++ ps/trunk/source/simulation2/components/ICmpProjectileManager.h (revision 20676)
@@ -1,54 +1,57 @@
/* Copyright (C) 2010 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_ICMPPROJECTILEMANAGER
#define INCLUDED_ICMPPROJECTILEMANAGER
#include "simulation2/system/Interface.h"
#include "maths/Fixed.h"
#include "maths/FixedVector3D.h"
/**
* Projectile manager. This deals with the rendering and the graphical motion of projectiles.
* (The gameplay effects of projectiles are not handled here - the simulation code does that
* with timers, this just does the visual aspects, because it's simpler to keep the parts separated.)
*/
class ICmpProjectileManager : public IComponent
{
public:
/**
* Launch a projectile from entity @p source to point @p target.
* @param source source entity; the projectile will determined from the "projectile" prop in its actor
* @param target target point
* @param speed horizontal speed in m/s
* @param gravity gravitational acceleration in m/s^2 (determines the height of the ballistic curve)
+ * @param actorName name of the flying projectile actor
+ * @param impactActorName name of the animation actor played when the projectile hits the target or the ground
+ * @param impactAnimationLifetime animation lenth
* @return id of the created projectile
*/
- virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, const CFixedVector3D& target, fixed speed, fixed gravity) = 0;
+ virtual uint32_t LaunchProjectileAtPoint(const CFixedVector3D& launchPoint, const CFixedVector3D& target, fixed speed, fixed gravity, const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime) = 0;
/**
* Removes a projectile, used when the projectile has hit a target
* @param id of the projectile to remove
*/
virtual void RemoveProjectile(uint32_t id) = 0;
DECLARE_INTERFACE_TYPE(ProjectileManager)
};
#endif // INCLUDED_ICMPPROJECTILEMANAGER
Index: ps/trunk/source/simulation2/components/ICmpVisual.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpVisual.cpp (revision 20675)
+++ ps/trunk/source/simulation2/components/ICmpVisual.cpp (revision 20676)
@@ -1,38 +1,40 @@
/* 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 "ICmpVisual.h"
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(Visual)
DEFINE_INTERFACE_METHOD_2("SetVariant", void, ICmpVisual, SetVariant, CStr, CStr)
DEFINE_INTERFACE_METHOD_CONST_0("GetAnimationName", std::string, ICmpVisual, GetAnimationName)
+DEFINE_INTERFACE_METHOD_CONST_0("GetProjectileActor", std::wstring, ICmpVisual, GetProjectileActor)
+DEFINE_INTERFACE_METHOD_CONST_0("GetProjectileLaunchPoint", CFixedVector3D, ICmpVisual, GetProjectileLaunchPoint)
DEFINE_INTERFACE_METHOD_4("SelectAnimation", void, ICmpVisual, SelectAnimation, std::string, bool, fixed, std::wstring)
DEFINE_INTERFACE_METHOD_1("SelectMovementAnimation", void, ICmpVisual, SelectMovementAnimation, fixed)
DEFINE_INTERFACE_METHOD_1("ResetMoveAnimation", void, ICmpVisual, ResetMoveAnimation, std::string)
DEFINE_INTERFACE_METHOD_2("ReplaceMoveAnimation", void, ICmpVisual, ReplaceMoveAnimation, std::string, std::string)
DEFINE_INTERFACE_METHOD_1("SetAnimationSyncRepeat", void, ICmpVisual, SetAnimationSyncRepeat, fixed)
DEFINE_INTERFACE_METHOD_1("SetAnimationSyncOffset", void, ICmpVisual, SetAnimationSyncOffset, fixed)
DEFINE_INTERFACE_METHOD_4("SetShadingColor", void, ICmpVisual, SetShadingColor, fixed, fixed, fixed, fixed)
DEFINE_INTERFACE_METHOD_2("SetVariable", void, ICmpVisual, SetVariable, std::string, float)
DEFINE_INTERFACE_METHOD_CONST_0("GetActorSeed", u32, ICmpVisual, GetActorSeed)
DEFINE_INTERFACE_METHOD_1("SetActorSeed", void, ICmpVisual, SetActorSeed, u32)
DEFINE_INTERFACE_METHOD_CONST_0("HasConstructionPreview", bool, ICmpVisual, HasConstructionPreview)
END_INTERFACE_WRAPPER(Visual)
Index: ps/trunk/source/simulation2/components/ICmpVisual.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpVisual.h (revision 20675)
+++ ps/trunk/source/simulation2/components/ICmpVisual.h (revision 20676)
@@ -1,183 +1,185 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_ICMPVISUAL
#define INCLUDED_ICMPVISUAL
#include "simulation2/system/Interface.h"
#include "ps/CStr.h"
#include "maths/BoundingBoxOriented.h"
#include "maths/BoundingBoxAligned.h"
#include "maths/Fixed.h"
+#include "maths/FixedVector3D.h"
#include "lib/file/vfs/vfs_path.h"
class CUnit;
/**
* The visual representation of an entity (typically an actor).
*/
class ICmpVisual : public IComponent
{
public:
/**
* Get the world-space bounding box of the object's visual representation.
* (Not safe for use in simulation code.)
*/
virtual CBoundingBoxAligned GetBounds() const = 0;
/**
* Get the oriented world-space bounding box of the object's visual representation, clipped at the Y=0 plane in object space
* to prevent it from extending into the terrain. The primary difference with GetBounds is that this bounding box is not aligned
* to the world axes, but arbitrarily rotated according to the model transform.
*/
virtual CBoundingBoxOriented GetSelectionBox() const = 0;
/**
* Get the world-space position of the base point of the object's visual representation.
* (Not safe for use in simulation code.)
*/
virtual CVector3D GetPosition() const = 0;
/**
* Return the short name of the actor that's being displayed, or the empty string on error.
* (Not safe for use in simulation code.)
*/
virtual std::wstring GetActorShortName() const = 0;
/**
* Return the filename of the actor to be used for projectiles from this unit, or the empty string if none.
* (Not safe for use in simulation code.)
*/
virtual std::wstring GetProjectileActor() const = 0;
/**
* Return the exact position where a projectile should be launched from (based on the actor's
* ammo prop points).
+ * Return type is CFixedVector3D because it is exposed to the JS interface.
* Returns (0,0,0) if no point can be found.
*/
- virtual CVector3D GetProjectileLaunchPoint() const = 0;
+ virtual CFixedVector3D GetProjectileLaunchPoint() const = 0;
/**
* Returns the underlying unit of this visual actor. May return NULL to indicate that no unit exists (e.g. may happen if the
* game is started without graphics rendering).
* Originally intended for introspection purposes in Atlas; for other purposes, consider using a specialized getter first.
*/
virtual CUnit* GetUnit() = 0;
/**
* Set the variant selection of the actor for a certain key.
* This overrides a previous selection on that key, so every component
* should use unique keys.
*/
virtual void SetVariant(const CStr& key, const CStr& selection) = 0;
/**
* Returns the name of the currently played animation.
*/
virtual std::string GetAnimationName() const = 0;
/**
* Start playing the given animation. If there are multiple possible animations then it will
* pick one at random (not network-synchronised).
* If @p soundgroup is specified, then the sound will be played at each 'event' point in the
* animation cycle.
* @param name animation name (e.g. "idle", "walk", "melee"; the names are determined by actor XML files)
* @param once if true then the animation will play once and freeze at the final frame, else it will loop
* @param speed animation speed multiplier (typically 1.0 for the default speed)
* @param soundgroup VFS path of sound group .xml, relative to audio/, or empty string for none
*/
virtual void SelectAnimation(const std::string& name, bool once, fixed speed, const std::wstring& soundgroup) = 0;
/**
* Replaces a specified animation with another. Only affects the special speed-based
* animation determination behaviour.
* @param name Animation to match.
* @param replace Animation that should replace the matched animation.
*/
virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace) = 0;
/**
* Ensures that the given animation will be used when it normally would be,
* removing reference to any animation that might replace it.
* @param name Animation name to remove from the replacement map.
*/
virtual void ResetMoveAnimation(const std::string& name) = 0;
/**
* Start playing the walk/run animations, scaled to the unit's movement speed.
* @param runThreshold movement speed at which to switch to the run animation
*/
virtual void SelectMovementAnimation(fixed runThreshold) = 0;
/**
* Adjust the speed of the current animation, so it can match simulation events.
* @param repeattime time for complete loop of animation, in msec
*/
virtual void SetAnimationSyncRepeat(fixed repeattime) = 0;
/**
* Adjust the offset of the current animation, so it can match simulation events.
* @param actiontime time between now and when the 'action' event should occur, in msec
*/
virtual void SetAnimationSyncOffset(fixed actiontime) = 0;
/**
* Set the shading color that will be modulated with the model's textures.
* Default shading is (1, 1, 1, 1).
* Alpha should probably be 1 else it's unlikely to work properly.
* @param r red component, expected range [0, 1]
* @param g green component, expected range [0, 1]
* @param b blue component, expected range [0, 1]
* @param a alpha component, expected range [0, 1]
*/
virtual void SetShadingColor(fixed r, fixed g, fixed b, fixed a) = 0;
/**
* Set an arbitrarily-named variable that the model may use to alter its appearance
* (e.g. in particle emitter parameter computations).
*/
virtual void SetVariable(const std::string& name, float value) = 0;
/**
* Get actor seed used for random variations
*/
virtual u32 GetActorSeed() const = 0;
/**
* Set actor seed for random variations and reload model
*/
virtual void SetActorSeed(u32 seed) = 0;
/**
* Returns true if this entity should have a construction preview
*/
virtual bool HasConstructionPreview() const = 0;
/**
* Called when an actor file has been modified and reloaded dynamically.
* If this component uses the named actor file, it should regenerate its actor
* to pick up the new definitions.
*/
virtual void Hotload(const VfsPath& name) = 0;
DECLARE_INTERFACE_TYPE(Visual)
};
// TODO: rename this to VisualActor, because the interface is actor-specific, maybe?
#endif // INCLUDED_ICMPVISUAL