Merge pull request #6821 from penev92/bleed_sandworm

Closes #2245
This commit is contained in:
Matthias Mailänder
2014-12-13 14:16:55 +01:00
21 changed files with 482 additions and 36 deletions

View File

@@ -0,0 +1,59 @@
#region Copyright & License Information
/*
* Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation. For more information,
* see COPYING.
*/
#endregion
using OpenRA.Mods.RA;
using OpenRA.Traits;
namespace OpenRA.Mods.D2k
{
[Desc("Sandworms use this attack model.")]
class AttackSwallowInfo : AttackFrontalInfo
{
[Desc("The number of ticks it takes to return underground.")]
public int ReturnTime = 60;
[Desc("The number of ticks it takes to get in place under the target to attack.")]
public int AttackTime = 30;
public readonly string WormAttackNotification = "WormAttack";
public override object Create(ActorInitializer init) { return new AttackSwallow(init.self, this); }
}
class AttackSwallow : AttackFrontal
{
new public readonly AttackSwallowInfo Info;
public AttackSwallow(Actor self, AttackSwallowInfo info)
: base(self, info)
{
Info = info;
}
public override void DoAttack(Actor self, Target target)
{
// This is so that the worm does not launch an attack against a target that has reached solid rock
if (target.Type != TargetType.Actor || !CanAttack(self, target))
{
self.CancelActivity();
return;
}
var a = ChooseArmamentForTarget(target);
if (a == null)
return;
if (!target.IsInRange(self.CenterPosition, a.Weapon.Range))
return;
self.CancelActivity();
self.QueueActivity(new SwallowActor(self, target, a.Weapon));
}
}
}

View File

@@ -68,6 +68,8 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="AttackSwallow.cs" />
<Compile Include="SwallowActor.cs" />
<Compile Include="SpriteLoaders\R8Loader.cs" />
<Compile Include="Traits\Buildings\DamagedWithoutFoundation.cs" />
<Compile Include="Traits\Render\WithBuildingPlacedOverlay.cs" />
@@ -89,6 +91,7 @@
<Compile Include="AutoCarryall\AutoCarryall.cs" />
<Compile Include="AutoCarryall\Carryable.cs" />
<Compile Include="AutoCarryall\CarryUnit.cs" />
<Compile Include="WormManager.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<PropertyGroup>

View File

@@ -0,0 +1,138 @@
#region Copyright & License Information
/*
* Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation. For more information,
* see COPYING.
*/
#endregion
using System.Drawing;
using System.Linq;
using OpenRA.GameRules;
using OpenRA.Mods.Common;
using OpenRA.Mods.Common.Traits;
using OpenRA.Mods.RA.Move;
using OpenRA.Traits;
namespace OpenRA.Mods.D2k
{
enum AttackState { Burrowed, EmergingAboveGround, ReturningUnderground }
class SwallowActor : Activity
{
const int NearEnough = 1;
readonly CPos location;
readonly Target target;
readonly WeaponInfo weapon;
readonly RenderUnit renderUnit;
readonly RadarPings radarPings;
readonly AttackSwallow swallow;
readonly IPositionable positionable;
int countdown;
AttackState stance;
public SwallowActor(Actor self, Target target, WeaponInfo weapon)
{
this.target = target;
this.weapon = weapon;
positionable = self.Trait<Mobile>();
swallow = self.Trait<AttackSwallow>();
renderUnit = self.Trait<RenderUnit>();
radarPings = self.World.WorldActor.TraitOrDefault<RadarPings>();
countdown = swallow.Info.AttackTime;
renderUnit.DefaultAnimation.ReplaceAnim("burrowed");
stance = AttackState.Burrowed;
location = target.Actor.Location;
}
bool WormAttack(Actor worm)
{
var targetLocation = target.Actor.Location;
// The target has moved too far away
if ((location - targetLocation).Length > NearEnough)
return false;
var lunch = worm.World.ActorMap.GetUnitsAt(targetLocation)
.Where(t => !t.Equals(worm) && weapon.IsValidAgainst(t, worm));
if (!lunch.Any())
return false;
stance = AttackState.EmergingAboveGround;
foreach (var actor in lunch)
actor.World.AddFrameEndTask(_ => actor.Destroy());
positionable.SetPosition(worm, targetLocation);
PlayAttackAnimation(worm);
var attackPosition = worm.CenterPosition;
var affectedPlayers = lunch.Select(x => x.Owner).Distinct();
foreach (var affectedPlayer in affectedPlayers)
NotifyPlayer(affectedPlayer, attackPosition);
return true;
}
void PlayAttackAnimation(Actor self)
{
renderUnit.PlayCustomAnim(self, "sand");
renderUnit.PlayCustomAnim(self, "mouth");
}
void NotifyPlayer(Player player, WPos location)
{
Sound.PlayNotification(player.World.Map.Rules, player, "Speech", swallow.Info.WormAttackNotification, player.Country.Race);
radarPings.Add(() => true, location, Color.Red, 50);
}
public override Activity Tick(Actor self)
{
if (countdown > 0)
{
countdown--;
return this;
}
if (stance == AttackState.ReturningUnderground) // Wait for the worm to get back underground
{
if (self.World.SharedRandom.Next() % 2 == 0) // There is a 50-50 chance that the worm would just go away
{
self.CancelActivity();
self.World.AddFrameEndTask(w => w.Remove(self));
var wormManager = self.World.WorldActor.TraitOrDefault<WormManager>();
if (wormManager != null)
wormManager.DecreaseWorms();
}
else
{
renderUnit.DefaultAnimation.ReplaceAnim("idle");
}
return NextActivity;
}
if (stance == AttackState.Burrowed) // Wait for the worm to get in position
{
// This is so that the worm cancels an attack against a target that has reached solid rock
if (!positionable.CanEnterCell(target.Actor.Location, null, false))
return NextActivity;
var success = WormAttack(self);
if (!success)
{
renderUnit.DefaultAnimation.ReplaceAnim("idle");
return NextActivity;
}
countdown = swallow.Info.ReturnTime;
stance = AttackState.ReturningUnderground;
}
return this;
}
}
}

View File

@@ -0,0 +1,122 @@
#region Copyright & License Information
/*
* Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation. For more information,
* see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.D2k
{
[Desc("Controls the spawning of sandworms. Attach this to the world actor.")]
class WormManagerInfo : ITraitInfo
{
[Desc("Minimum number of worms")]
public readonly int Minimum = 2;
[Desc("Maximum number of worms")]
public readonly int Maximum = 4;
[Desc("Average time (seconds) between worm spawn")]
public readonly int SpawnInterval = 120;
public readonly string WormSignNotification = "WormSign";
public readonly string WormSignature = "sandworm";
public readonly string WormOwnerPlayer = "Creeps";
public object Create(ActorInitializer init) { return new WormManager(this, init.self); }
}
class WormManager : ITick
{
int countdown;
int wormsPresent;
readonly WormManagerInfo info;
readonly Lazy<Actor[]> spawnPoints;
readonly Lazy<RadarPings> radarPings;
public WormManager(WormManagerInfo info, Actor self)
{
this.info = info;
radarPings = Exts.Lazy(() => self.World.WorldActor.Trait<RadarPings>());
spawnPoints = Exts.Lazy(() => self.World.ActorsWithTrait<WormSpawner>().Select(x => x.Actor).ToArray());
}
public void Tick(Actor self)
{
// TODO: Add a lobby option to disable worms just like crates
// TODO: It would be even better to stop
if (!spawnPoints.Value.Any())
return;
// Apparantly someone doesn't want worms or the maximum number of worms has been reached
if (info.Maximum < 1 || wormsPresent >= info.Maximum)
return;
if (--countdown > 0 && wormsPresent >= info.Minimum)
return;
countdown = info.SpawnInterval * 25;
var wormLocations = new List<WPos>();
wormLocations.Add(SpawnWorm(self));
while (wormsPresent < info.Minimum)
wormLocations.Add(SpawnWorm(self));
AnnounceWormSign(self, wormLocations);
}
WPos SpawnWorm(Actor self)
{
var spawnPoint = GetRandomSpawnPoint(self);
self.World.AddFrameEndTask(w => w.CreateActor(info.WormSignature, new TypeDictionary
{
new OwnerInit(w.Players.First(x => x.PlayerName == info.WormOwnerPlayer)),
new LocationInit(spawnPoint.Location)
}));
wormsPresent++;
return spawnPoint.CenterPosition;
}
Actor GetRandomSpawnPoint(Actor self)
{
return spawnPoints.Value.Random(self.World.SharedRandom);
}
public void DecreaseWorms()
{
wormsPresent--;
}
void AnnounceWormSign(Actor self, IEnumerable<WPos> wormLocations)
{
if (self.World.LocalPlayer != null)
Sound.PlayNotification(self.World.Map.Rules, self.World.LocalPlayer, "Speech", info.WormSignNotification, self.World.LocalPlayer.Country.Race);
if (radarPings.Value == null)
return;
foreach (var wormLocation in wormLocations)
radarPings.Value.Add(() => true, wormLocation, Color.Red, 50);
}
}
[Desc("An actor with this trait indicates a valid spawn point for sandworms.")]
class WormSpawnerInfo : TraitInfo<WormSpawner> { }
class WormSpawner { }
}

View File

@@ -26,6 +26,9 @@ namespace OpenRA.Mods.RA
public readonly string Cursor = "attack";
public readonly string OutsideRangeCursor = "attackoutsiderange";
[Desc("Does the attack type require the attacker to enter the target's cell?")]
public readonly bool AttackRequiresEnteringCell = false;
public abstract object Create(ActorInitializer init);
}
@@ -35,15 +38,16 @@ namespace OpenRA.Mods.RA
public IEnumerable<Armament> Armaments { get { return GetArmaments(); } }
protected Lazy<IFacing> facing;
protected Lazy<Building> building;
protected Lazy<IPositionable> positionable;
protected Func<IEnumerable<Armament>> GetArmaments;
readonly Actor self;
readonly AttackBaseInfo info;
public readonly AttackBaseInfo Info;
public AttackBase(Actor self, AttackBaseInfo info)
{
this.self = self;
this.info = info;
Info = info;
var armaments = Exts.Lazy(() => self.TraitsImplementing<Armament>()
.Where(a => info.Armaments.Contains(a.Info.Name)));
@@ -52,6 +56,7 @@ namespace OpenRA.Mods.RA
facing = Exts.Lazy(() => self.TraitOrDefault<IFacing>());
building = Exts.Lazy(() => self.TraitOrDefault<Building>());
positionable = Exts.Lazy(() => self.Trait<IPositionable>());
}
protected virtual bool CanAttack(Actor self, Target target)
@@ -59,6 +64,9 @@ namespace OpenRA.Mods.RA
if (!self.IsInWorld)
return false;
if (!HasAnyValidWeapons(target))
return false;
// Building is under construction or is being sold
if (building.Value != null && !building.Value.BuildComplete)
return false;
@@ -135,7 +143,17 @@ namespace OpenRA.Mods.RA
public abstract Activity GetAttackActivity(Actor self, Target newTarget, bool allowMove);
public bool HasAnyValidWeapons(Target t) { return Armaments.Any(a => a.Weapon.IsValidAgainst(t, self.World, self)); }
public bool HasAnyValidWeapons(Target t)
{
if (Info.AttackRequiresEnteringCell)
{
if (!positionable.Value.CanEnterCell(t.Actor.Location, null, false))
return false;
}
return Armaments.Any(a => a.Weapon.IsValidAgainst(t, self.World, self));
}
public WRange GetMaximumRange()
{
return Armaments.Select(a => a.Weapon.Range).Append(WRange.Zero).Max();
@@ -179,8 +197,8 @@ namespace OpenRA.Mods.RA
var a = ab.ChooseArmamentForTarget(target);
cursor = a != null && !target.IsInRange(self.CenterPosition, a.Weapon.Range)
? ab.info.OutsideRangeCursor
: ab.info.Cursor;
? ab.Info.OutsideRangeCursor
: ab.Info.Cursor;
if (target.Type == TargetType.Actor && target.Actor == self)
return false;
@@ -210,7 +228,7 @@ namespace OpenRA.Mods.RA
IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue);
cursor = ab.info.Cursor;
cursor = ab.Info.Cursor;
if (negativeDamage)
return false;
@@ -223,7 +241,7 @@ namespace OpenRA.Mods.RA
var maxRange = ab.GetMaximumRange().Range;
var targetRange = (self.World.Map.CenterOfCell(location) - self.CenterPosition).HorizontalLengthSquared;
if (targetRange > maxRange * maxRange)
cursor = ab.info.OutsideRangeCursor;
cursor = ab.Info.OutsideRangeCursor;
return true;
}

View File

@@ -15,25 +15,48 @@ namespace OpenRA.Mods.RA
{
[Desc("Will AttackMove to a random location within MoveRadius when idle.",
"This conflicts with player orders and should only be added to animal creeps.")]
class AttackWanderInfo : ITraitInfo
class AttackWanderInfo : ITraitInfo, Requires<AttackMoveInfo>
{
public readonly int MoveRadius = 4;
public readonly int WanderMoveRadius = 10;
[Desc("Number of ticks to wait until decreasing the effective move radius.")]
public readonly int MoveReductionRadiusScale = 5;
public object Create(ActorInitializer init) { return new AttackWander(init.self, this); }
}
class AttackWander : INotifyIdle
{
int ticksIdle;
int effectiveMoveRadius;
readonly AttackMove attackMove;
readonly AttackWanderInfo Info;
public AttackWander(Actor self, AttackWanderInfo info)
{
Info = info;
effectiveMoveRadius = info.WanderMoveRadius;
attackMove = self.TraitOrDefault<AttackMove>();
}
public void TickIdle(Actor self)
{
var target = self.CenterPosition + new WVec(0, -1024*Info.MoveRadius, 0).Rotate(WRot.FromFacing(self.World.SharedRandom.Next(255)));
self.Trait<AttackMove>().ResolveOrder(self, new Order("AttackMove", self, false) { TargetLocation = self.World.Map.CellContaining(target) });
var target = self.CenterPosition + new WVec(0, -1024 * effectiveMoveRadius, 0).Rotate(WRot.FromFacing(self.World.SharedRandom.Next(255)));
var targetCell = self.World.Map.CellContaining(target);
if (!self.World.Map.Contains(targetCell))
{
// If MoveRadius is too big there might not be a valid cell to order the attack to (if actor is on a small island and can't leave)
if (++ticksIdle % Info.MoveReductionRadiusScale == 0)
effectiveMoveRadius--;
return; // We'll be back the next tick; better to sit idle for a few seconds than prolongue this tick indefinitely with a loop
}
attackMove.ResolveOrder(self, new Order("AttackMove", self, false) { TargetLocation = targetCell });
ticksIdle = 0;
effectiveMoveRadius = Info.WanderMoveRadius;
}
}
}

View File

@@ -49,7 +49,8 @@ namespace OpenRA.Mods.RA.Traits
public void TickIdle(Actor self)
{
if (TargetLocation.HasValue)
// This might cause the actor to be stuck if the target location is unreachable
if (TargetLocation.HasValue && self.Location != TargetLocation.Value)
Activate(self);
}

View File

@@ -458,6 +458,7 @@ VICE:
MuzzleSplitFacings: 8
AttackFrontal:
AttackWander:
WanderMoveRadius: 2
RenderUnit:
WithMuzzleFlash:
SplitFacings: true

BIN
mods/d2k/bits/wormicon.shp Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -33,13 +33,17 @@ Players:
Name: Atreides
Race: atreides
ColorRamp: 161,134,200
Allies: Neutral
Enemies: Harkonnen
PlayerReference@Harkonnen:
Name: Harkonnen
Race: harkonnen
ColorRamp: 3,255,127
Enemies: Atreides
PlayerReference@Creeps:
Name: Creeps
NonCombatant: True
Race: atreides
Enemies: Atreides, Harkonnen
Actors:
Actor4: spicebloom
@@ -102,6 +106,9 @@ Actors:
Actor41: guntowera
Location: 46,39
Owner: Harkonnen
Actor42: wormspawner
Location: 46,64
Owner: Creeps
Smudges:
@@ -114,6 +121,8 @@ Rules:
-MPStartLocations:
ResourceType@Spice:
ValuePerUnit: 0
WormManager:
Minimum: 1
Sequences:

View File

@@ -36,6 +36,7 @@ Rules:
./mods/d2k/rules/atreides.yaml
./mods/d2k/rules/harkonnen.yaml
./mods/d2k/rules/ordos.yaml
./mods/d2k/rules/arrakis.yaml
Sequences:
./mods/d2k/sequences/aircraft.yaml

View File

@@ -31,6 +31,8 @@ Speech:
UnitLost: ULOST
BuildingLost: BLOST
BuildingCaptured: CAPT
WormSign: WSIGN
WormAttack: WATTK
Sounds:
DefaultVariant: .WAV

View File

@@ -0,0 +1,53 @@
SPICEBLOOM:
RenderBuilding:
Building:
Footprint: x
Dimensions: 1,1
AppearsOnRadar:
EditorAppearance:
RelativeToTopLeft: yes
ProximityCaptor:
Types: Tree
Tooltip:
Name: Spice Bloom
SeedsResource:
ResourceType: Spice
Interval: 75
WithActiveAnimation:
RadarColorFromTerrain:
Terrain: Spice
BodyOrientation:
WithMakeAnimation:
SANDWORM:
Tooltip:
Name: Sandworm
Description: Attracted by vibrations in the sand.\nWill eat units whole and has a large appetite.
Health:
HP: 10000
Radius: 3
Armor:
Type: None
Mobile:
Speed: 50
TerrainSpeeds:
Sand: 100
Dune: 100
Spice: 100
TargetableUnit:
TargetTypes: Underground
RevealsShroud:
Range: 32c0
RenderUnit:
BodyOrientation:
HiddenUnderFog:
AppearsOnRadar:
UseLocation: yes
AttackSwallow:
AttackRequiresEnteringCell: true
AttackMove:
AttackWander:
AutoTarget:
ScanRadius: 32
Armament:
Weapon: WormJaw

View File

@@ -140,27 +140,6 @@ waypoint:
RenderEditorOnly:
BodyOrientation:
SPICEBLOOM:
RenderBuilding:
Building:
Footprint: x
Dimensions: 1,1
AppearsOnRadar:
EditorAppearance:
RelativeToTopLeft: yes
ProximityCaptor:
Types: Tree
Tooltip:
Name: Spice Bloom
SeedsResource:
ResourceType: Spice
Interval: 75
WithActiveAnimation:
RadarColorFromTerrain:
Terrain: Spice
BodyOrientation:
WithMakeAnimation:
CAMERA:
Immobile:
OccupiesSpace: false
@@ -170,3 +149,9 @@ CAMERA:
Range: 8c0
BodyOrientation:
wormspawner:
Immobile:
OccupiesSpace: false
RenderEditorOnly:
BodyOrientation:
WormSpawner:

View File

@@ -10,6 +10,7 @@ World:
ScreenShaker:
BuildingInfluence:
ChooseBuildTabOnSelect:
WormManager:
CrateSpawner:
Minimum: 0
Maximum: 2

View File

@@ -441,4 +441,22 @@ grenadier: # 2502 - 2749 in 1.06 DATA.R8
Facings: 8
Tick: 120
icon: grenadiericon
Start: 0 # 4281 in 1.06 DATA.R8
Start: 0 # 4281 in 1.06 DATA.R8
sandworm:
mouth: DATA
Start: 3549
Length: 15
Tick: 100
sand: DATA
Start: 3565
Length: 20
idle: DATA
Start: 3586
Length: 35
Tick: 180
BlendMode: Additive
burrowed: DATA
Start: 39
icon: wormicon
Start: 0

View File

@@ -267,6 +267,11 @@ waypoint:
Start: 0
Length: *
wormspawner:
idle:
Start: 0
Length: *
sietch:
idle: DATA
Start: 2998

View File

@@ -78,4 +78,10 @@ SaboteurVoice:
Select: O_SSEL1,O_SSEL2,O_SSEL3
Move: O_SCONF1,O_SCONF2,O_SCONF3
Die: KILLGUY1,KILLGUY2,KILLGUY3,KILLGUY4,KILLGUY5,KILLGUY6,KILLGUY7,KILLGUY8,KILLGUY9
DisableVariants: Select, Move
DisableVariants: Select, Move
WormVoice:
DefaultVariant: .WAV
Voices:
Select: WRMSIGN1
Move: WORM

View File

@@ -583,6 +583,7 @@ DOGGIE:
Weapon: FiendShard
AttackFrontal:
AttackWander:
WanderMoveRadius: 2
VISSML:
Inherits: ^Infantry