diff --git a/OpenRA.Mods.D2k/AttackSwallow.cs b/OpenRA.Mods.D2k/AttackSwallow.cs new file mode 100644 index 0000000000..a5c56bc4bf --- /dev/null +++ b/OpenRA.Mods.D2k/AttackSwallow.cs @@ -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)); + } + } +} diff --git a/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj b/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj index 1cb2379b4f..3edfdb132e 100644 --- a/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj +++ b/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj @@ -68,6 +68,8 @@ + + @@ -89,6 +91,7 @@ + diff --git a/OpenRA.Mods.D2k/SwallowActor.cs b/OpenRA.Mods.D2k/SwallowActor.cs new file mode 100644 index 0000000000..db2c49baea --- /dev/null +++ b/OpenRA.Mods.D2k/SwallowActor.cs @@ -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(); + swallow = self.Trait(); + renderUnit = self.Trait(); + radarPings = self.World.WorldActor.TraitOrDefault(); + 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(); + 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; + } + } +} diff --git a/OpenRA.Mods.D2k/WormManager.cs b/OpenRA.Mods.D2k/WormManager.cs new file mode 100644 index 0000000000..867d68e5c8 --- /dev/null +++ b/OpenRA.Mods.D2k/WormManager.cs @@ -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 spawnPoints; + readonly Lazy radarPings; + + public WormManager(WormManagerInfo info, Actor self) + { + this.info = info; + radarPings = Exts.Lazy(() => self.World.WorldActor.Trait()); + spawnPoints = Exts.Lazy(() => self.World.ActorsWithTrait().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(); + + 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 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 { } + class WormSpawner { } +} diff --git a/OpenRA.Mods.RA/Attack/AttackBase.cs b/OpenRA.Mods.RA/Attack/AttackBase.cs index b96321daad..89ba53efc7 100644 --- a/OpenRA.Mods.RA/Attack/AttackBase.cs +++ b/OpenRA.Mods.RA/Attack/AttackBase.cs @@ -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 Armaments { get { return GetArmaments(); } } protected Lazy facing; protected Lazy building; + protected Lazy positionable; protected Func> 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() .Where(a => info.Armaments.Contains(a.Info.Name))); @@ -52,6 +56,7 @@ namespace OpenRA.Mods.RA facing = Exts.Lazy(() => self.TraitOrDefault()); building = Exts.Lazy(() => self.TraitOrDefault()); + positionable = Exts.Lazy(() => self.Trait()); } 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; } diff --git a/OpenRA.Mods.RA/Attack/AttackWander.cs b/OpenRA.Mods.RA/Attack/AttackWander.cs index bb173f1509..b9174fe3e2 100644 --- a/OpenRA.Mods.RA/Attack/AttackWander.cs +++ b/OpenRA.Mods.RA/Attack/AttackWander.cs @@ -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 { - 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(); } 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().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; } } } diff --git a/OpenRA.Mods.RA/Traits/AttackMove.cs b/OpenRA.Mods.RA/Traits/AttackMove.cs index 8be8e7b75d..4e750ad777 100644 --- a/OpenRA.Mods.RA/Traits/AttackMove.cs +++ b/OpenRA.Mods.RA/Traits/AttackMove.cs @@ -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); } diff --git a/mods/cnc/rules/civilian.yaml b/mods/cnc/rules/civilian.yaml index 34871fa770..2e07980ef2 100644 --- a/mods/cnc/rules/civilian.yaml +++ b/mods/cnc/rules/civilian.yaml @@ -458,6 +458,7 @@ VICE: MuzzleSplitFacings: 8 AttackFrontal: AttackWander: + WanderMoveRadius: 2 RenderUnit: WithMuzzleFlash: SplitFacings: true diff --git a/mods/d2k/bits/wormicon.shp b/mods/d2k/bits/wormicon.shp new file mode 100644 index 0000000000..fcc29f93f7 Binary files /dev/null and b/mods/d2k/bits/wormicon.shp differ diff --git a/mods/d2k/bits/wormspawner.shp b/mods/d2k/bits/wormspawner.shp new file mode 100644 index 0000000000..cd1a7e79ae Binary files /dev/null and b/mods/d2k/bits/wormspawner.shp differ diff --git a/mods/d2k/maps/death-depths.oramap b/mods/d2k/maps/death-depths.oramap index 1603fe195f..4b5832580d 100644 Binary files a/mods/d2k/maps/death-depths.oramap and b/mods/d2k/maps/death-depths.oramap differ diff --git a/mods/d2k/maps/shellmap/map.yaml b/mods/d2k/maps/shellmap/map.yaml index affdc50af0..0d28aee1ae 100644 --- a/mods/d2k/maps/shellmap/map.yaml +++ b/mods/d2k/maps/shellmap/map.yaml @@ -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: diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 65e32652f7..6d3018f755 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -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 diff --git a/mods/d2k/notifications.yaml b/mods/d2k/notifications.yaml index 56c3dffc63..7548e642e2 100644 --- a/mods/d2k/notifications.yaml +++ b/mods/d2k/notifications.yaml @@ -31,6 +31,8 @@ Speech: UnitLost: ULOST BuildingLost: BLOST BuildingCaptured: CAPT + WormSign: WSIGN + WormAttack: WATTK Sounds: DefaultVariant: .WAV diff --git a/mods/d2k/rules/arrakis.yaml b/mods/d2k/rules/arrakis.yaml new file mode 100644 index 0000000000..43e718f53e --- /dev/null +++ b/mods/d2k/rules/arrakis.yaml @@ -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 \ No newline at end of file diff --git a/mods/d2k/rules/misc.yaml b/mods/d2k/rules/misc.yaml index 12257edc36..57084a2a1c 100644 --- a/mods/d2k/rules/misc.yaml +++ b/mods/d2k/rules/misc.yaml @@ -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: diff --git a/mods/d2k/rules/world.yaml b/mods/d2k/rules/world.yaml index 26dd5e7d75..7b70b48083 100644 --- a/mods/d2k/rules/world.yaml +++ b/mods/d2k/rules/world.yaml @@ -10,6 +10,7 @@ World: ScreenShaker: BuildingInfluence: ChooseBuildTabOnSelect: + WormManager: CrateSpawner: Minimum: 0 Maximum: 2 diff --git a/mods/d2k/sequences/infantry.yaml b/mods/d2k/sequences/infantry.yaml index 8c64824a8e..e828e28d57 100644 --- a/mods/d2k/sequences/infantry.yaml +++ b/mods/d2k/sequences/infantry.yaml @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/mods/d2k/sequences/misc.yaml b/mods/d2k/sequences/misc.yaml index 45530895db..a4dcf64b14 100644 --- a/mods/d2k/sequences/misc.yaml +++ b/mods/d2k/sequences/misc.yaml @@ -267,6 +267,11 @@ waypoint: Start: 0 Length: * +wormspawner: + idle: + Start: 0 + Length: * + sietch: idle: DATA Start: 2998 diff --git a/mods/d2k/voices.yaml b/mods/d2k/voices.yaml index 4357f91e70..1f44df603e 100644 --- a/mods/d2k/voices.yaml +++ b/mods/d2k/voices.yaml @@ -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 \ No newline at end of file + DisableVariants: Select, Move + +WormVoice: + DefaultVariant: .WAV + Voices: + Select: WRMSIGN1 + Move: WORM \ No newline at end of file diff --git a/mods/ts/rules/infantry.yaml b/mods/ts/rules/infantry.yaml index 476c4fda9a..78422e3035 100644 --- a/mods/ts/rules/infantry.yaml +++ b/mods/ts/rules/infantry.yaml @@ -583,6 +583,7 @@ DOGGIE: Weapon: FiendShard AttackFrontal: AttackWander: + WanderMoveRadius: 2 VISSML: Inherits: ^Infantry