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