1
0
forked from mirrors/0ad

Added randomized arrow positions with hit detection when the missile lands and splash damage. Fixes #18.

This was SVN commit r11886.
This commit is contained in:
quantumstate
2012-05-19 23:07:41 +00:00
parent 6cc98a6763
commit f72d820cd4
33 changed files with 503 additions and 204 deletions
@@ -70,6 +70,7 @@ Attack.prototype.Schema =
"<PrepareTime>800</PrepareTime>" +
"<RepeatTime>1600</RepeatTime>" +
"<ProjectileSpeed>50.0</ProjectileSpeed>" +
"<Spread>2.5</Spread>" +
"<Bonuses>" +
"<Bonus1>" +
"<Classes>Cavalry</Classes>" +
@@ -77,6 +78,14 @@ Attack.prototype.Schema =
"</Bonus1>" +
"</Bonuses>" +
"<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
"<Splash>" +
"<Shape>Circular</Shape>" +
"<Range>20</Range>" +
"<FriendlyFire>false</FriendlyFire>" +
"<Hack>0.0</Hack>" +
"<Pierce>10.0</Pierce>" +
"<Crush>0.0</Crush>" +
"</Splash>" +
"</Ranged>" +
"<Charge>" +
"<Hack>10.0</Hack>" +
@@ -119,9 +128,23 @@ Attack.prototype.Schema =
"<element name='ProjectileSpeed' a:help='Speed of projectiles (in metres per second). If unspecified, then it is a melee attack instead'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='Spread' a:help='Radius over which missiles will tend to land. Roughly 2/3 will land inside this radius (in metres)'><ref name='nonNegativeDecimal'/></element>" +
bonusesSchema +
preferredClassesSchema +
restrictedClassesSchema +
"<optional>" +
"<element name='Splash'>" +
"<interleave>" +
"<element name='Shape' a:help='Shape of the splash damage, can be circular or linear'><text/></element>" +
"<element name='Range' a:help='Size of the area affected by the splash'><ref name='nonNegativeDecimal'/></element>" +
"<element name='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" +
"<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
bonusesSchema +
"</interleave>" +
"</element>" +
"</optional>" +
"</interleave>" +
"</element>" +
"</optional>" +
@@ -287,6 +310,14 @@ Attack.prototype.GetAttackStrengths = function(type)
// Work out the attack values with technology effects
var self = this;
var template = this.template[type];
var splash = "";
if (!template)
{
template = this.template[type.split(".")[0]].Splash;
splash = "/Splash";
}
var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager);
var applyTechs = function(damageType)
{
@@ -294,8 +325,8 @@ Attack.prototype.GetAttackStrengths = function(type)
if (cmpTechMan)
{
// All causes caching problems so disable it for now.
//var allComponent = cmpTechMan.ApplyModifications("Attack/" + type + "/All", strength, self.entity) - self.template[type][damageType];
strength = cmpTechMan.ApplyModifications("Attack/" + type + "/" + damageType, strength, self.entity);
//var allComponent = cmpTechMan.ApplyModifications("Attack/" + type + splash + "/All", strength, self.entity) - self.template[type][damageType];
strength = cmpTechMan.ApplyModifications("Attack/" + type + splash + "/" + damageType, strength, self.entity);
}
return strength;
};
@@ -326,16 +357,20 @@ Attack.prototype.GetRange = function(type)
Attack.prototype.GetAttackBonus = function(type, target)
{
var attackBonus = 1;
if (this.template[type].Bonuses)
var template = this.template[type];
if (!template)
template = this.template[type.split(".")[0]].Splash;
if (template.Bonuses)
{
var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return 1;
// Multiply the bonuses for all matching classes
for (var key in this.template[type].Bonuses)
for (var key in template.Bonuses)
{
var bonus = this.template[type].Bonuses[key];
var bonus = template.Bonuses[key];
var hasClasses = true;
if (bonus.Classes){
@@ -352,6 +387,20 @@ Attack.prototype.GetAttackBonus = function(type, target)
return attackBonus;
};
// Returns a 2d random distribution scaled for a spread of scale 1.
// The current implementation is a 2d gaussian with sigma = 1
Attack.prototype.GetNormalDistribution = function(){
// Use the Box-Muller transform to get a gaussian distribution
var a = Math.random();
var b = Math.random();
var c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b);
var d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b);
return [c, d];
};
/**
* 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
@@ -362,32 +411,18 @@ Attack.prototype.PerformAttack = function(type, target)
// If this is a ranged attack, then launch a projectile
if (type == "Ranged")
{
// To implement (in)accuracy, for arrows and javelins, we want to do the following:
// * Compute an accuracy rating, based on the entity's characteristics and the distance to the target
// * Pick a random point 'close' to the target (based on the accuracy) which is the real target point
// * Pick a real target unit, based on their footprint's proximity to the real target point
// * If there is none, then harmlessly shoot to the real target point instead
// * If the real target unit moves after being targeted, the projectile will follow it and hit it anyway
//
// In the future this should be extended:
// * If the target unit moves too far, the projectile should 'detach' and not hit it, so that
// players can dodge projectiles. (Or it should pick a new target after detaching, so it can still
// hit somebody.)
// 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
// * There should be more control over the probabilities of hitting enemy units vs friendly units vs missing,
// for gameplay balance tweaks
// * Larger, slower projectiles (catapults etc) shouldn't pick targets first, they should just
// hurt anybody near their landing point
// Get some data about the entity
var horizSpeed = +this.template[type].ProjectileSpeed;
var gravity = 9.81; // this affects the shape of the curve; assume it's constant for now
var accuracy = 6; // TODO: get from entity template
//horizSpeed /= 8; gravity /= 8; // slow it down for testing
// Find the distance to the target
var spread = this.template.Ranged.Spread;
//horizSpeed /= 2; gravity /= 2; // slow it down for testing
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
@@ -396,48 +431,47 @@ Attack.prototype.PerformAttack = function(type, target)
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
var targetPosition = cmpTargetPosition.GetPosition();
var horizDistance = Math.sqrt(Math.pow(targetPosition.x - selfPosition.x, 2) + Math.pow(targetPosition.z - selfPosition.z, 2));
// Compute the real target point (based on accuracy)
var angle = Math.random() * 2*Math.PI;
var r = 1 - Math.sqrt(Math.random()); // triangular distribution [0,1] (cluster around the center)
var offset = r * accuracy; // TODO: should be affected by range
var offsetX = offset * Math.sin(angle);
var offsetZ = offset * Math.cos(angle);
var realTargetPosition = { "x": targetPosition.x + offsetX, "y": targetPosition.y, "z": targetPosition.z + offsetZ };
// TODO: what we should really do here is select the unit whose footprint is closest to the realTargetPosition
// (and harmlessly hit the ground if there's none), but as a simplification let's just randomly decide whether to
// hit the original target or not.
var realTargetUnit = undefined;
if (Math.random() < 0.5) // TODO: this is yucky and hardcoded
{
// Hit the original target
realTargetUnit = target;
realTargetPosition = targetPosition;
}
else
{
// Hit the ground
// TODO: ought to make sure Y is on the ground
}
// Hurt the target after the appropriate time
if (realTargetUnit)
{
var realHorizDistance = Math.sqrt(Math.pow(realTargetPosition.x - selfPosition.x, 2) + Math.pow(realTargetPosition.z - selfPosition.z, 2));
var timeToTarget = realHorizDistance / horizSpeed;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.SetTimeout(this.entity, IID_Attack, "CauseDamage", timeToTarget*1000, {"type": type, "target": target});
}
var relativePosition = {"x": targetPosition.x - selfPosition.x, "z": targetPosition.z - selfPosition.z}
var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
var targetVelocity = {"x": (targetPosition.x - previousTargetPosition.x) / this.turnLength, "z": (targetPosition.z - previousTargetPosition.z) / this.turnLength}
// the component of the targets velocity radially away from the archer
var radialSpeed = this.VectorDot(relativePosition, targetVelocity) / this.VectorLength(relativePosition);
var horizDistance = this.VectorDistance(targetPosition, selfPosition);
// This is an approximation of the time ot the target, it assumes that the target has a constant radial
// velocity, but since units move in straight lines this is not true. The exact value would be more
// difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was
// about 5% of the units radius out in the worst case)
var timeToTarget = horizDistance / (horizSpeed - radialSpeed);
// Predict where the unit is when the missile lands.
var predictedPosition = {"x": targetPosition.x + targetVelocity.x * timeToTarget,
"z": targetPosition.z + targetVelocity.z * timeToTarget};
// Compute the real target point (based on spread and target speed)
var randNorm = this.GetNormalDistribution();
var offsetX = randNorm[0] * spread * (1 + this.VectorLength(targetVelocity) / 20);
var offsetZ = randNorm[1] * spread * (1 + this.VectorLength(targetVelocity) / 20);
var realTargetPosition = { "x": predictedPosition.x + offsetX, "y": targetPosition.y, "z": predictedPosition.z + offsetZ };
// Calculate when the missile will hit the target position
var realHorizDistance = this.VectorDistance(realTargetPosition, selfPosition);
var timeToTarget = realHorizDistance / horizSpeed;
var missileDirection = {"x": (realTargetPosition.x - selfPosition.x) / realHorizDistance, "z": (realTargetPosition.z - selfPosition.z) / realHorizDistance};
// Make the arrow appear to land slightly behind the target so that arrows landing next to a guys foot don't count but arrows that go through the torso do
var graphicalPosition = {"x": realTargetPosition.x + 2*missileDirection.x, "y": realTargetPosition.y + 2*missileDirection.y};
// Launch the graphical projectile
var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
if (realTargetUnit)
cmpProjectileManager.LaunchProjectileAtEntity(this.entity, realTargetUnit, horizSpeed, gravity);
else
cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
var id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id});
}
else
{
@@ -465,6 +499,170 @@ Attack.prototype.TargetKilled = function(killerEntity, targetEntity)
}
};
Attack.prototype.InterpolatedLocation = function(ent, lateness)
{
var targetPositionCmp = Engine.QueryInterface(ent, IID_Position);
if (!targetPositionCmp) // TODO: handle dead target properly
return undefined;
var curPos = targetPositionCmp.GetPosition();
var prevPos = targetPositionCmp.GetPreviousPosition();
lateness /= 1000;
return {"x": (curPos.x * (this.turnLength - lateness) + prevPos.x * lateness) / this.turnLength,
"z": (curPos.z * (this.turnLength - lateness) + prevPos.z * lateness) / this.turnLength};
};
Attack.prototype.VectorDistance = function(p1, p2)
{
return Math.sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.z - p2.z)*(p1.z - p2.z));
};
Attack.prototype.VectorDot = function(p1, p2)
{
return (p1.x * p2.x + p1.z * p2.z);
};
Attack.prototype.VectorCross = function(p1, p2)
{
return (p1.x * p2.z - p1.z * p2.x);
};
Attack.prototype.VectorLength = function(p)
{
return Math.sqrt(p.x*p.x + p.z*p.z);
};
// Tests whether it point is inside of ent's footprint
Attack.prototype.testCollision = function(ent, point, lateness)
{
var targetPosition = this.InterpolatedLocation(ent, lateness);
var targetShape = Engine.QueryInterface(ent, IID_Footprint).GetShape();
if (!targetShape || !targetPosition)
return false;
if (targetShape.type === 'circle')
{
return (this.VectorDistance(point, targetPosition) < targetShape.radius);
}
else
{
var targetRotation = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
var dx = point.x - targetPosition.x;
var dz = point.z - targetPosition.z;
var dxr = Math.cos(targetRotation) * dx - Math.sin(targetRotation) * dz;
var dzr = Math.sin(targetRotation) * dx + Math.cos(targetRotation) * dz;
return (-targetShape.width/2 <= dxr && dxr < targetShape.width/2 && -targetShape.depth/2 <= dzr && dzr < targetShape.depth/2);
}
};
Attack.prototype.MissileHit = function(data, lateness)
{
var targetPosition = this.InterpolatedLocation(data.target, lateness);
if (!targetPosition)
return;
if (this.template.Ranged.Splash) // splash damage, do this first in case the direct hit kills the target
{
var friendlyFire = this.template.Ranged.Splash.FriendlyFire;
var splashRadius = this.template.Ranged.Splash.Range;
var splashShape = this.template.Ranged.Splash.Shape;
var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2 + splashRadius, friendlyFire);
ents.push(data.target); // Add the original unit to the list of splash damage targets
for (var i = 0; i < ents.length; i++)
{
var entityPosition = this.InterpolatedLocation(ents[i], lateness);
var radius = this.VectorDistance(data.position, entityPosition);
if (radius < splashRadius)
{
var multiplier = 1;
if (splashShape == "Circular") // quadratic falloff
{
multiplier *= 1 - ((radius * radius) / (splashRadius * splashRadius));
}
else if (splashShape == "Linear")
{
// position of entity relative to where the missile hit
var relPos = {"x": entityPosition.x - data.position.x, "z": entityPosition.z - data.position.z};
var splashWidth = splashRadius / 5;
var parallelDist = this.VectorDot(relPos, data.direction);
var perpDist = Math.abs(this.VectorCross(relPos, data.direction));
// Check that the unit is within the distance splashWidth of the line starting at the missile's
// landing point which extends in the direction of the missile for length splashRadius.
if (parallelDist > -splashWidth && perpDist < splashWidth)
{
// Use a quadratic falloff in both directions
multiplier = (splashRadius*splashRadius - parallelDist*parallelDist) / (splashRadius*splashRadius)
* (splashWidth*splashWidth - perpDist*perpDist) / (splashWidth*splashWidth);
}
else
{
multiplier = 0;
}
}
var newData = {"type": data.type + ".Splash", "target": ents[i], "damageMultiplier": multiplier};
this.CauseDamage(newData);
}
}
}
if (this.testCollision(data.target, data.position, lateness))
{
// Hit the primary target
this.CauseDamage(data);
// Remove the projectile
var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
cmpProjectileManager.RemoveProjectile(data.projectileId);
}
else
{
// If we didn't hit the main target look for nearby units
var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2);
for (var i = 0; i < ents.length; i++)
{
if (this.testCollision(ents[i], data.position, lateness))
{
var newData = {"type": data.type, "target": ents[i]};
this.CauseDamage(newData);
// Remove the projectile
var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
cmpProjectileManager.RemoveProjectile(data.projectileId);
}
}
}
};
Attack.prototype.GetNearbyEntities = function(startEnt, range, friendlyFire)
{
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var owner = cmpOwnership.GetOwner();
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player);
var numPlayers = cmpPlayerManager.GetNumPlayers();
var players = [];
for (var i = 1; i < numPlayers; ++i)
{
// Only target enemies unless friendly fire is on
if (cmpPlayer.IsEnemy(i) || friendlyFire)
players.push(i);
}
var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return rangeManager.ExecuteQuery(startEnt, 0, range, players, IID_DamageReceiver);
}
/**
* Inflict damage on the target
*/
@@ -472,12 +670,14 @@ Attack.prototype.CauseDamage = function(data)
{
var strengths = this.GetAttackStrengths(data.type);
var attackBonus = this.GetAttackBonus(data.type, data.target);
var damageMultiplier = this.GetAttackBonus(data.type, data.target);
if (data.damageMultiplier !== undefined)
damageMultiplier *= data.damageMultiplier;
var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
if (!cmpDamageReceiver)
return;
var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * attackBonus, strengths.pierce * attackBonus, strengths.crush * attackBonus);
var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * damageMultiplier, strengths.pierce * damageMultiplier, strengths.crush * damageMultiplier);
// if target killed pick up loot and credit experience
if (targetState.killed == true)
{
@@ -490,4 +690,9 @@ Attack.prototype.CauseDamage = function(data)
PlaySound("attack_impact", this.entity);
};
Attack.prototype.OnUpdate = function(msg)
{
this.turnLength = msg.turnLength;
}
Engine.RegisterComponentType(IID_Attack, "Attack", Attack);
@@ -10,6 +10,7 @@
<ProjectileSpeed>60.0</ProjectileSpeed>
<PrepareTime>0</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<Identity>
@@ -15,6 +15,7 @@
<ProjectileSpeed>75.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<BuildingAI>
@@ -15,6 +15,7 @@
<ProjectileSpeed>75.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<BuildingAI>
@@ -15,6 +15,7 @@
<ProjectileSpeed>75.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<BuildingAI>
@@ -15,6 +15,7 @@
<ProjectileSpeed>75.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<BuildingAI>
@@ -15,6 +15,7 @@
<ProjectileSpeed>75.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<BuildingAI>
@@ -9,8 +9,7 @@
<Classes datatype="tokens">Town Wall</Classes>
<GenericName>City Wall</GenericName>
<Tooltip>Wall off your town for a stout defense.</Tooltip>
<Icon>structures/wall.png</Icon>
<RequiredTechnology>phase_town</RequiredTechnology>
<RequiredTechnology>phase_town</RequiredTechnology>
</Identity>
<WallSet>
<MaxTowerOverlap>0.85</MaxTowerOverlap>
@@ -38,11 +38,6 @@
<Obstruction>
<Static width="30.0" depth="26.0"/>
</Obstruction>
<ProductionQueue>
<Technologies datatype="tokens">
armor_trade_convoys
</Technologies>
</ProductionQueue>
<Sound>
<SoundGroups>
<select>interface/select/building/sel_market.xml</select>
@@ -56,6 +51,9 @@
<Weight>65536</Weight>
</TerritoryInfluence>
<ProductionQueue>
<Technologies datatype="tokens">
armor_trade_convoys
</Technologies>
<Entities datatype="tokens">
units/{civ}_support_trader
</Entities>
@@ -15,6 +15,7 @@
<ProjectileSpeed>75.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<BuildingAI>
@@ -12,6 +12,7 @@
<RepeatTime>1500</RepeatTime>
<PreferredClasses datatype="tokens">Organic</PreferredClasses>
<RestrictedClasses datatype="tokens">StoneWall</RestrictedClasses>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<Identity>
@@ -15,16 +15,17 @@
<ProjectileSpeed>50.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Bonuses>
<BonusPrimary>
<Classes>Infantry Bow</Classes>
<Multiplier>2.0</Multiplier>
</BonusPrimary>
<BonusSecondary>
<Classes>Cavalry Sword</Classes>
<Multiplier>1.5</Multiplier>
</BonusSecondary>
</Bonuses>
<Spread>1.5</Spread>
<Bonuses>
<BonusPrimary>
<Classes>Infantry Bow</Classes>
<Multiplier>2.0</Multiplier>
</BonusPrimary>
<BonusSecondary>
<Classes>Cavalry Sword</Classes>
<Multiplier>1.5</Multiplier>
</BonusSecondary>
</Bonuses>
<RestrictedClasses datatype="tokens">StoneWall</RestrictedClasses>
</Ranged>
</Attack>
@@ -15,16 +15,17 @@
<ProjectileSpeed>75.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Bonuses>
<BonusPrimary>
<Classes>Infantry Sword</Classes>
<Multiplier>2.0</Multiplier>
</BonusPrimary>
<BonusSecondary>
<Classes>Cavalry Spear</Classes>
<Multiplier>1.5</Multiplier>
</BonusSecondary>
</Bonuses>
<Spread>1.5</Spread>
<Bonuses>
<BonusPrimary>
<Classes>Infantry Sword</Classes>
<Multiplier>2.0</Multiplier>
</BonusPrimary>
<BonusSecondary>
<Classes>Cavalry Spear</Classes>
<Multiplier>1.5</Multiplier>
</BonusSecondary>
</Bonuses>
<PreferredClasses datatype="tokens">Organic</PreferredClasses>
<RestrictedClasses datatype="tokens">StoneWall</RestrictedClasses>
</Ranged>
@@ -15,24 +15,25 @@
<ProjectileSpeed>50.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Bonuses>
<BonusPrimary>
<Classes>Infantry Spear</Classes>
<Multiplier>1.5</Multiplier>
</BonusPrimary>
<BonusSecondary>
<Classes>Cavalry Bow</Classes>
<Multiplier>1.5</Multiplier>
</BonusSecondary>
<BonusElephants>
<Classes>Elephant</Classes>
<Multiplier>1.5</Multiplier>
</BonusElephants>
<BonusChariots>
<Classes>Chariot</Classes>
<Multiplier>1.5</Multiplier>
</BonusChariots>
</Bonuses>
<Spread>1.5</Spread>
<Bonuses>
<BonusPrimary>
<Classes>Infantry Spear</Classes>
<Multiplier>1.5</Multiplier>
</BonusPrimary>
<BonusSecondary>
<Classes>Cavalry Bow</Classes>
<Multiplier>1.5</Multiplier>
</BonusSecondary>
<BonusElephants>
<Classes>Elephant</Classes>
<Multiplier>1.5</Multiplier>
</BonusElephants>
<BonusChariots>
<Classes>Chariot</Classes>
<Multiplier>1.5</Multiplier>
</BonusChariots>
</Bonuses>
<PreferredClasses datatype="tokens">Organic</PreferredClasses>
<RestrictedClasses datatype="tokens">StoneWall</RestrictedClasses>
</Ranged>
@@ -15,16 +15,17 @@
<ProjectileSpeed>75.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Bonuses>
<BonusPrimary>
<Classes>Infantry Spear</Classes>
<Multiplier>2.0</Multiplier>
</BonusPrimary>
<BonusSecondary>
<Classes>Infantry Sword</Classes>
<Multiplier>1.5</Multiplier>
</BonusSecondary>
</Bonuses>
<Spread>1.5</Spread>
<Bonuses>
<BonusPrimary>
<Classes>Infantry Spear</Classes>
<Multiplier>2.0</Multiplier>
</BonusPrimary>
<BonusSecondary>
<Classes>Infantry Sword</Classes>
<Multiplier>1.5</Multiplier>
</BonusSecondary>
</Bonuses>
</Ranged>
</Attack>
<Cost>
@@ -14,6 +14,7 @@
<ProjectileSpeed>25.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<Cost>
@@ -10,6 +10,7 @@
<ProjectileSpeed>28.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
<PreferredClasses datatype="tokens">Organic</PreferredClasses>
<RestrictedClasses datatype="tokens">StoneWall</RestrictedClasses>
</Ranged>
@@ -15,6 +15,7 @@
<ProjectileSpeed>60.0</ProjectileSpeed>
<PrepareTime>2000</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<BuildingAI>
@@ -15,6 +15,7 @@
<ProjectileSpeed>40.0</ProjectileSpeed>
<PrepareTime>2000</PrepareTime>
<RepeatTime>4000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<Cost>
@@ -15,6 +15,7 @@
<ProjectileSpeed>60.0</ProjectileSpeed>
<PrepareTime>1000</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<BuildingAI>
@@ -7,19 +7,28 @@
<Attack>
<Ranged>
<Hack>0.0</Hack>
<Pierce>50.0</Pierce>
<Crush>50.0</Crush>
<Pierce>20.0</Pierce>
<Crush>20.0</Crush>
<MaxRange>60</MaxRange>
<MinRange>8.0</MinRange>
<Spread>6.0</Spread>
<ProjectileSpeed>60.0</ProjectileSpeed>
<PrepareTime>5000</PrepareTime>
<RepeatTime>5000</RepeatTime>
<Bonuses>
<BonusOrganic>
<Classes>Organic</Classes>
<Multiplier>2.0</Multiplier>
</BonusOrganic>
</Bonuses>
<Bonuses>
<BonusOrganic>
<Classes>Organic</Classes>
<Multiplier>2.0</Multiplier>
</BonusOrganic>
</Bonuses>
<Splash>
<Shape>Linear</Shape>
<Range>12</Range>
<FriendlyFire>false</FriendlyFire>
<Hack>0.0</Hack>
<Pierce>30.0</Pierce>
<Crush>30.0</Crush>
</Splash>
</Ranged>
</Attack>
<Cost>
@@ -6,20 +6,29 @@
</Armour>
<Attack>
<Ranged>
<Hack>50.0</Hack>
<Hack>40.0</Hack>
<Pierce>0.0</Pierce>
<Crush>50.0</Crush>
<Crush>40.0</Crush>
<MaxRange>68</MaxRange>
<MinRange>12.0</MinRange>
<ProjectileSpeed>30.0</ProjectileSpeed>
<PrepareTime>5000</PrepareTime>
<RepeatTime>5000</RepeatTime>
<Spread>6.0</Spread>
<Bonuses>
<BonusStruct>
<Classes>Structure</Classes>
<Multiplier>2.0</Multiplier>
</BonusStruct>
</Bonuses>
<Splash>
<Shape>Circular</Shape>
<Range>12</Range>
<FriendlyFire>true</FriendlyFire>
<Hack>12.0</Hack>
<Pierce>0.0</Pierce>
<Crush>12.0</Crush>
</Splash>
</Ranged>
</Attack>
<Cost>
@@ -14,6 +14,7 @@
<ProjectileSpeed>75.0</ProjectileSpeed>
<PrepareTime>1200</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<BuildingAI>
@@ -10,6 +10,7 @@
<ProjectileSpeed>10.0</ProjectileSpeed>
<PrepareTime>2000</PrepareTime>
<RepeatTime>2000</RepeatTime>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<Footprint replace="">
@@ -944,9 +944,18 @@ bool CCmpObstructionManager::FindMostImportantObstruction(const IObstructionTest
// Then look for obstructions that cover the target point when expanded by r
// (i.e. if the target is not inside an object but closer than we can get to it)
// TODO: actually do that
// (This might matter when you tell a unit to walk too close to the edge of a building)
GetObstructionsInRange(filter, x-r, z-r, x+r, z+r, squares);
// Building squares are more important but returned last, so check backwards
for (std::vector<ObstructionSquare>::reverse_iterator it = squares.rbegin(); it != squares.rend(); ++it)
{
CFixedVector2D halfSize(it->hw + r, it->hh + r);
if (Geometry::PointIsInSquare(CFixedVector2D(it->x, it->z) - center, it->u, it->v, halfSize))
{
square = *it;
return true;
}
}
return false;
}
+48 -5
View File
@@ -66,7 +66,9 @@ public:
// Dynamic state:
bool m_InWorld;
entity_pos_t m_X, m_Z, m_LastX, m_LastZ; // these values contain undefined junk if !InWorld
// m_LastX/Z contain the position from the start of the most recent turn
// m_PrevX/Z conatain the position from the turn before that
entity_pos_t m_X, m_Z, m_LastX, m_LastZ, m_PrevX, m_PrevZ; // these values contain undefined junk if !InWorld
entity_pos_t m_YOffset;
bool m_RelativeToGround; // whether m_YOffset is relative to terrain/water plane, or an absolute height
@@ -201,8 +203,8 @@ public:
if (!m_InWorld)
{
m_InWorld = true;
m_LastX = m_X;
m_LastZ = m_Z;
m_LastX = m_PrevX = m_X;
m_LastZ = m_PrevZ = m_Z;
}
AdvertisePositionChanges();
@@ -210,8 +212,8 @@ public:
virtual void JumpTo(entity_pos_t x, entity_pos_t z)
{
m_LastX = m_X = x;
m_LastZ = m_Z = z;
m_LastX = m_PrevX = m_X = x;
m_LastZ = m_PrevZ = m_Z = z;
m_InWorld = true;
AdvertisePositionChanges();
@@ -278,6 +280,43 @@ public:
return CFixedVector2D(m_X, m_Z);
}
virtual CFixedVector3D GetPreviousPosition()
{
if (!m_InWorld)
{
LOGERROR(L"CCmpPosition::GetPreviousPosition called on entity when IsInWorld is false");
return CFixedVector3D();
}
entity_pos_t baseY;
if (m_RelativeToGround)
{
CmpPtr<ICmpTerrain> cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
if (cmpTerrain)
baseY = cmpTerrain->GetGroundLevel(m_PrevX, m_PrevZ);
if (m_Floating)
{
CmpPtr<ICmpWaterManager> cmpWaterMan(GetSimContext(), SYSTEM_ENTITY);
if (cmpWaterMan)
baseY = std::max(baseY, cmpWaterMan->GetWaterLevel(m_PrevX, m_PrevZ));
}
}
return CFixedVector3D(m_PrevX, baseY + m_YOffset, m_PrevZ);
}
virtual CFixedVector2D GetPreviousPosition2D()
{
if (!m_InWorld)
{
LOGERROR(L"CCmpPosition::GetPreviousPosition2D called on entity when IsInWorld is false");
return CFixedVector2D();
}
return CFixedVector2D(m_PrevX, m_PrevZ);
}
virtual void TurnTo(entity_angle_t y)
{
m_RotY = y;
@@ -408,6 +447,10 @@ public:
}
case MT_TurnStart:
{
// Store the positions from the turn before
m_PrevX = m_LastX;
m_PrevZ = m_LastZ;
m_LastX = m_X;
m_LastZ = m_Z;
@@ -20,6 +20,8 @@
#include "simulation2/system/Component.h"
#include "ICmpProjectileManager.h"
#include "ICmpObstruction.h"
#include "ICmpObstructionManager.h"
#include "ICmpPosition.h"
#include "ICmpRangeManager.h"
#include "ICmpTerrain.h"
@@ -58,6 +60,7 @@ public:
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_ActorSeed = 0;
m_Id = 1;
}
virtual void Deinit()
@@ -86,7 +89,7 @@ public:
case MT_Interpolate:
{
const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
Interpolate(msgData.frameTime, msgData.offset);
Interpolate(msgData.frameTime);
break;
}
case MT_RenderSubmit:
@@ -98,15 +101,12 @@ public:
}
}
virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_t target, fixed speed, fixed gravity)
virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity)
{
LaunchProjectile(source, CFixedVector3D(), target, speed, gravity);
}
virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity)
{
LaunchProjectile(source, target, INVALID_ENTITY, speed, gravity);
return LaunchProjectile(source, target, speed, gravity);
}
virtual void RemoveProjectile(uint32_t);
private:
struct Projectile
@@ -114,36 +114,38 @@ private:
CUnit* unit;
CVector3D pos;
CVector3D target;
entity_id_t targetEnt; // INVALID_ENTITY if the target is just a point
float timeLeft;
float speedFactor;
float gravity;
bool stopped;
uint32_t id;
};
std::vector<Projectile> m_Projectiles;
uint32_t m_ActorSeed;
uint32_t m_Id;
void LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, fixed speed, fixed gravity);
uint32_t LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity);
void AdvanceProjectile(Projectile& projectile, float dt, float frameOffset);
void AdvanceProjectile(Projectile& projectile, float dt);
void Interpolate(float frameTime, float frameOffset);
void Interpolate(float frameTime);
void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling);
};
REGISTER_COMPONENT_TYPE(ProjectileManager)
void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, fixed speed, fixed gravity)
uint32_t CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity)
{
if (!GetSimContext().HasUnitManager())
return; // do nothing if graphics are disabled
return 0; // do nothing if graphics are disabled
CmpPtr<ICmpVisual> cmpSourceVisual(GetSimContext(), source);
if (!cmpSourceVisual)
return;
return 0;
std::wstring name = cmpSourceVisual->GetProjectileActor();
if (name.empty())
@@ -151,7 +153,7 @@ void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D
// If the actor was actually loaded, complain that it doesn't have a projectile
if (!cmpSourceVisual->GetActorShortName().empty())
LOGERROR(L"Unit with actor '%ls' launched a projectile but has no actor on 'projectile' attachpoint", cmpSourceVisual->GetActorShortName().c_str());
return;
return 0;
}
CVector3D sourceVec(cmpSourceVisual->GetProjectileLaunchPoint());
@@ -161,7 +163,7 @@ void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D
CmpPtr<ICmpPosition> sourcePos(GetSimContext(), source);
if (!sourcePos)
return;
return 0;
sourceVec = sourcePos->GetPosition();
sourceVec.Y += 3.f;
@@ -169,31 +171,20 @@ void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D
CVector3D targetVec;
if (targetEnt == INVALID_ENTITY)
{
targetVec = CVector3D(targetPoint);
}
else
{
CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), targetEnt);
if (!cmpTargetPosition)
return;
targetVec = CVector3D(cmpTargetPosition->GetPosition());
}
targetVec = CVector3D(targetPoint);
Projectile projectile;
std::set<CStr> selections;
projectile.unit = GetSimContext().GetUnitManager().CreateUnit(name, m_ActorSeed++, selections);
projectile.id = m_Id++;
if (!projectile.unit)
{
// The error will have already been logged
return;
return 0;
}
projectile.pos = sourceVec;
projectile.target = targetVec;
projectile.targetEnt = targetEnt;
CVector3D offset = projectile.target - projectile.pos;
float horizDistance = sqrtf(offset.X*offset.X + offset.Z*offset.Z);
@@ -205,9 +196,11 @@ void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D
projectile.gravity = gravity.ToFloat();
m_Projectiles.push_back(projectile);
return projectile.id;
}
void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt, float frameOffset)
void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt)
{
// Do special processing if we've already reached the target
if (projectile.timeLeft <= 0)
@@ -223,24 +216,6 @@ void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt,
// apply a bit of drag to them
projectile.speedFactor *= powf(1.0f - 0.4f*projectile.speedFactor, dt);
}
else
{
// Projectile hasn't reached the target yet:
// Track the target entity (if there is one, and it's still alive)
if (projectile.targetEnt != INVALID_ENTITY)
{
CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), projectile.targetEnt);
if (cmpTargetPosition && cmpTargetPosition->IsInWorld())
{
CMatrix3D t = cmpTargetPosition->GetInterpolatedTransform(frameOffset, false);
projectile.target = t.GetTranslation();
projectile.target.Y += 2.f; // TODO: ought to aim towards a random point in the solid body of the target
// TODO: if the unit is moving, we should probably aim a bit in front of it
// so we don't have to curve so much just before reaching it
}
}
}
CVector3D offset = (projectile.target - projectile.pos) * projectile.speedFactor;
@@ -296,11 +271,11 @@ void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt,
projectile.unit->GetModel().SetTransform(transform);
}
void CCmpProjectileManager::Interpolate(float frameTime, float frameOffset)
void CCmpProjectileManager::Interpolate(float frameTime)
{
for (size_t i = 0; i < m_Projectiles.size(); ++i)
{
AdvanceProjectile(m_Projectiles[i], frameTime, frameOffset);
AdvanceProjectile(m_Projectiles[i], frameTime);
}
// Remove the ones that have reached their target
@@ -310,7 +285,7 @@ void CCmpProjectileManager::Interpolate(float frameTime, float frameOffset)
// Those hitting the ground stay for a while, because it looks pretty.
if (m_Projectiles[i].timeLeft <= 0.f)
{
if (m_Projectiles[i].targetEnt == INVALID_ENTITY && m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)
if (m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)
{
// Keep the projectile until it exceeds the decay time
}
@@ -328,6 +303,22 @@ void CCmpProjectileManager::Interpolate(float frameTime, float frameOffset)
}
}
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::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling)
{
CmpPtr<ICmpRangeManager> cmpRangeManager(GetSimContext(), SYSTEM_ENTITY);
@@ -32,6 +32,8 @@ DEFINE_INTERFACE_METHOD_1("SetHeightFixed", void, ICmpPosition, SetHeightFixed,
DEFINE_INTERFACE_METHOD_0("IsFloating", bool, ICmpPosition, IsFloating)
DEFINE_INTERFACE_METHOD_0("GetPosition", CFixedVector3D, ICmpPosition, GetPosition)
DEFINE_INTERFACE_METHOD_0("GetPosition2D", CFixedVector2D, ICmpPosition, GetPosition2D)
DEFINE_INTERFACE_METHOD_0("GetPreviousPosition", CFixedVector3D, ICmpPosition, GetPreviousPosition)
DEFINE_INTERFACE_METHOD_0("GetPreviousPosition2D", CFixedVector2D, ICmpPosition, GetPreviousPosition2D)
DEFINE_INTERFACE_METHOD_1("TurnTo", void, ICmpPosition, TurnTo, entity_angle_t)
DEFINE_INTERFACE_METHOD_1("SetYRotation", void, ICmpPosition, SetYRotation, entity_angle_t)
DEFINE_INTERFACE_METHOD_2("SetXZRotation", void, ICmpPosition, SetXZRotation, entity_angle_t, entity_angle_t)
@@ -108,6 +108,19 @@ public:
*/
virtual CFixedVector2D GetPosition2D() = 0;
/**
* Returns the previous turn's x,y,z position (no interpolation).
* Depends on the current terrain heightmap.
* Must not be called unless IsInWorld is true.
*/
virtual CFixedVector3D GetPreviousPosition() = 0;
/**
* Returns the previous turn's x,z position (no interpolation).
* Must not be called unless IsInWorld is true.
*/
virtual CFixedVector2D GetPreviousPosition2D() = 0;
/**
* Rotate smoothly to the given angle around the upwards axis.
* @param y clockwise radians from the +Z axis.
@@ -22,6 +22,6 @@
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(ProjectileManager)
DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtEntity", void, ICmpProjectileManager, LaunchProjectileAtEntity, entity_id_t, entity_id_t, fixed, fixed)
DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", void, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed)
DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", uint32_t, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed)
DEFINE_INTERFACE_METHOD_1("RemoveProjectile", void, ICmpProjectileManager, RemoveProjectile, uint32_t)
END_INTERFACE_WRAPPER(ProjectileManager)
@@ -31,14 +31,6 @@
class ICmpProjectileManager : public IComponent
{
public:
/**
* Launch a projectile from entity @p source to entity @p target.
* @param source source entity; the projectile will determined from the "projectile" prop in its actor
* @param target target entity; the projectile will automatically track the target to ensure it always hits precisely
* @param speed horizontal speed in m/s
* @param gravity gravitational acceleration in m/s^2 (determines the height of the ballistic curve)
*/
virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_t target, fixed speed, fixed gravity) = 0;
/**
* Launch a projectile from entity @p source to point @p target.
@@ -46,8 +38,15 @@ public:
* @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)
* @return id of the created projectile
*/
virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity) = 0;
virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity) = 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)
};
@@ -50,6 +50,8 @@ public:
virtual bool IsFloating() { return false; }
virtual CFixedVector3D GetPosition() { return CFixedVector3D(); }
virtual CFixedVector2D GetPosition2D() { return CFixedVector2D(); }
virtual CFixedVector3D GetPreviousPosition() { return CFixedVector3D(); }
virtual CFixedVector2D GetPreviousPosition2D() { return CFixedVector2D(); }
virtual void TurnTo(entity_angle_t UNUSED(y)) { }
virtual void SetYRotation(entity_angle_t UNUSED(y)) { }
virtual void SetXZRotation(entity_angle_t UNUSED(x), entity_angle_t UNUSED(z)) { }
+2 -1
View File
@@ -395,7 +395,8 @@ exposed (in template files etc) with the name "ExampleTwo", and implementing the
The @c Init and @c Deinit functions are optional. Unlike C++, there are no @c Serialize/Deserialize functions -
each JS component instance is automatically serialized and restored.
(This automatic serialization restricts what you can store as properties in the object - e.g. you cannot store function closures,
because they're too hard to serialize. The details should be documented on some other page eventually.)
because they're too hard to serialize. This will serialize Strings, numbers, bools, null, undefined, arrays of serializable
values whose property names are purely numeric, objects whose properties are serializable values. Cyclic structures are allowed.)
Instead of @c ClassInit and @c HandleMessage, you simply add functions of the form <code>On<var>MessageType</var></code>.
(If you want the equivalent of SubscribeGloballyToMessageType, then use <code>OnGlobal<var>MessageType</var></code> instead.)