diff --git a/OpenRA.Mods.D2k/AttackSwallow.cs b/OpenRA.Mods.D2k/AttackSwallow.cs new file mode 100644 index 0000000000..f0a2b6dcc7 --- /dev/null +++ b/OpenRA.Mods.D2k/AttackSwallow.cs @@ -0,0 +1,54 @@ +#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 +{ + // TODO: This is a copy of AttackLeap. Maybe combine them in AttackMelee trait when the code is finalized? + [Desc("Sandworms use this attack model.")] + class AttackSwallowInfo : AttackFrontalInfo, Requires + { + public override object Create(ActorInitializer init) { return new AttackSwallow(init.self, this); } + } + class AttackSwallow : AttackFrontal + { + readonly Sandworm sandworm; + + public AttackSwallow(Actor self, AttackSwallowInfo attackSwallowInfo) + : base(self, attackSwallowInfo) + { + sandworm = self.Trait(); + } + + public override void DoAttack(Actor self, Target target) + { + // TODO: Worm should ignore Fremen as targets unless they are firing/being fired upon (even moving fremen do not attract worms) + + if (target.Type != TargetType.Actor || !CanAttack(self, target) || !sandworm.CanAttackAtLocation(self, target.Actor.Location)) + // this is so that the worm does not launch an attack against a target that has reached solid rock + { + 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.Actor, a.Weapon)); + } + } +} diff --git a/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj b/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj index 1cb2379b4f..ad6db3439f 100644 --- a/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj +++ b/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj @@ -68,6 +68,11 @@ + + + + + @@ -89,6 +94,13 @@ + + + + + + + diff --git a/OpenRA.Mods.D2k/Sandworm.cs b/OpenRA.Mods.D2k/Sandworm.cs new file mode 100644 index 0000000000..2bc5c08b38 --- /dev/null +++ b/OpenRA.Mods.D2k/Sandworm.cs @@ -0,0 +1,70 @@ +#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.Mods.RA.Move; +using OpenRA.Mods.RA.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.D2k +{ + class SandwormInfo : Requires, Requires, IOccupySpaceInfo + { + readonly public int WanderMoveRadius = 20; + readonly public string WormSignNotification = "WormSign"; + + public object Create(ActorInitializer init) { return new Sandworm(this); } + } + + class Sandworm : INotifyIdle + { + int ticksIdle; + int effectiveMoveRadius; + readonly int maxMoveRadius; + + public Sandworm(SandwormInfo info) + { + maxMoveRadius = info.WanderMoveRadius; + effectiveMoveRadius = info.WanderMoveRadius; + + // TODO: Someone familiar with how the sounds work should fix this: + // TODO: This should not be here. It should be same as "Enemy unit sighted". + //Sound.PlayNotification(self.Owner, "Speech", info.WormSignNotification, self.Owner.Country.Race); + } + + // TODO: This copies AttackWander and builds on top of it. AttackWander should be revised. + public void TickIdle(Actor self) + { + var globalOffset = new WVec(0, -1024 * effectiveMoveRadius, 0).Rotate(WRot.FromFacing(self.World.SharedRandom.Next(255))); + var offset = new CVec(globalOffset.X/1024, globalOffset.Y/1024); + var targetlocation = self.Location + offset; + + if (!self.World.Map.Bounds.Contains(targetlocation.X, targetlocation.Y)) + { + // 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 % 10 == 0) // completely random number + { + 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 + } + + self.World.IssueOrder(new Order("AttackMove", self, false) { TargetLocation = targetlocation }); + + ticksIdle = 0; + effectiveMoveRadius = maxMoveRadius; + } + + public bool CanAttackAtLocation(Actor self, CPos targetLocation) + { + return self.Trait().MovementSpeedForCell(self, targetLocation) != 0; + } + } +} diff --git a/OpenRA.Mods.D2k/SwallowActor.cs b/OpenRA.Mods.D2k/SwallowActor.cs new file mode 100644 index 0000000000..cb9c18ccec --- /dev/null +++ b/OpenRA.Mods.D2k/SwallowActor.cs @@ -0,0 +1,136 @@ +#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.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.RA.Move; +using OpenRA.Mods.RA.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.D2k +{ + public enum AttackState { Burrowed, EmergingAboveGround, ReturningUndergrown } + + class SwallowActor : Activity + { + readonly Actor target; + readonly Mobile mobile; + readonly Sandworm sandworm; + readonly WeaponInfo weapon; + + int countdown; + AttackState stance = AttackState.Burrowed; + + // TODO: random numbers to make it look ok + [Desc("The number of ticks it takes to return underground.")] + const int ReturnTime = 60; + [Desc("The number of ticks it takes to get in place under the target to attack.")] + const int AttackTime = 30; + + public SwallowActor(Actor self, Actor target, WeaponInfo weapon) + { + if (!target.HasTrait()) + throw new InvalidOperationException("SwallowActor requires a target actor with the Mobile trait"); + + this.target = target; + this.weapon = weapon; + mobile = self.TraitOrDefault(); + sandworm = self.TraitOrDefault(); + countdown = AttackTime; + } + + bool WormAttack(Actor worm) + { + var targetLocation = target.Location; + + var lunch = worm.World.ActorMap.GetUnitsAt(targetLocation) + .Except(new[] { worm }) + .Where(t => weapon.IsValidAgainst(t, worm)); + if (!lunch.Any()) + return false; + + stance = AttackState.EmergingAboveGround; + + lunch.Do(t => t.World.AddFrameEndTask(_ => { t.World.Remove(t); t.Kill(t); })); // dispose of the evidence (we don't want husks) + + mobile.SetPosition(worm, targetLocation); + PlayAttackAnimation(worm); + + return true; + } + + public bool PlayAttackAnimation(Actor self) + { + var renderUnit = self.Trait(); + renderUnit.PlayCustomAnim(self, "sand"); + renderUnit.PlayCustomAnim(self, "mouth"); + + // TODO: Someone familiar with how the sounds work should fix this: + //Sound.PlayNotification(self.Owner, "Speech", "WormAttack", self.Owner.Country.Race); + + return true; + } + + public override Activity Tick(Actor self) + { + if (countdown > 0) + { + countdown--; + return this; + } + + if (stance == AttackState.ReturningUndergrown) // wait for the worm to get back underground + { + #region DisappearToMapEdge + + // More random numbers used for min and max bounds + var rand = self.World.SharedRandom.Next(200, 400); + if (rand % 2 == 0) // there is a 50-50 chance that the worm would just go away + { + self.CancelActivity(); + //self.World.WorldActor.QueueActivity(new DisappearToMapEdge(self, rand)); + self.World.AddFrameEndTask(w => w.Remove(self)); + var wormManager = self.World.WorldActor.TraitOrDefault(); + if (wormManager != null) + wormManager.DecreaseWorms(); + } + + #endregion + + // TODO: if the worm did not disappear, make the animation reappear here + + return NextActivity; + } + + if (stance == AttackState.Burrowed) // wait for the worm to get in position + { + // TODO: make the worm animation (currenty the lightning) disappear here + + // this is so that the worm cancels an attack against a target that has reached solid rock + if (sandworm == null || !sandworm.CanAttackAtLocation(self, target.Location)) + { + return NextActivity; + } + + var success = WormAttack(self); + if (!success) + { + return NextActivity; + } + + countdown = ReturnTime; + stance = AttackState.ReturningUndergrown; + } + + return this; + } + } +} diff --git a/OpenRA.Mods.D2k/WormManager.cs b/OpenRA.Mods.D2k/WormManager.cs new file mode 100644 index 0000000000..af36094fe7 --- /dev/null +++ b/OpenRA.Mods.D2k/WormManager.cs @@ -0,0 +1,87 @@ +#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.Linq; +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 = 1; + + [Desc("Maximum number of worms")] + public readonly int Maximum = 5; + + [Desc("Average time (seconds) between crate spawn")] + public readonly int SpawnInterval = 180; + + 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; + + public WormManager(WormManagerInfo info, Actor self) + { + this.info = info; + 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 + + if (--countdown > 0) + return; + + countdown = info.SpawnInterval * 25; + if (wormsPresent < info.Maximum) + SpawnWorm(self); + } + + private void SpawnWorm (Actor self) + { + var spawnLocation = GetRandomSpawnPosition(self); + self.World.AddFrameEndTask(w => + w.CreateActor(info.WormSignature, new TypeDictionary + { + new OwnerInit(w.Players.First(x => x.PlayerName == info.WormOwnerPlayer)), + new LocationInit(spawnLocation) + })); + wormsPresent++; + } + + private CPos GetRandomSpawnPosition(Actor self) + { + // TODO: This is here only for testing, while the maps don't have valid spawn points + if (!spawnPoints.Value.Any()) + return self.World.Map.ChooseRandomEdgeCell(self.World.SharedRandom); + + return spawnPoints.Value[self.World.SharedRandom.Next(0, spawnPoints.Value.Count() - 1)].Location; + } + + public void DecreaseWorms() + { + wormsPresent--; + } + } +} diff --git a/OpenRA.Mods.D2k/WormSpawner.cs b/OpenRA.Mods.D2k/WormSpawner.cs new file mode 100644 index 0000000000..99730e79b4 --- /dev/null +++ b/OpenRA.Mods.D2k/WormSpawner.cs @@ -0,0 +1,24 @@ +#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.Traits; + +namespace OpenRA.Mods.D2k +{ + [Desc("An actor with this trait indicates a valid spawn point for sandworms.")] + class WormSpawnerInfo : ITraitInfo + { + public object Create(ActorInitializer init) { return new WormSpawner(); } + } + + class WormSpawner + { + } +} diff --git a/OpenRA.Mods.RA/Attack/AttackBase.cs b/OpenRA.Mods.RA/Attack/AttackBase.cs index b96321daad..1e5971d7c9 100644 --- a/OpenRA.Mods.RA/Attack/AttackBase.cs +++ b/OpenRA.Mods.RA/Attack/AttackBase.cs @@ -25,6 +25,8 @@ namespace OpenRA.Mods.RA public readonly string Cursor = "attack"; public readonly string OutsideRangeCursor = "attackoutsiderange"; + [Desc("Does the attack type requires the attacker to enter the target's cell?")] + public readonly bool AttackRequiresEnteringCell = false; public abstract object Create(ActorInitializer init); } @@ -38,12 +40,12 @@ namespace OpenRA.Mods.RA 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))); @@ -179,8 +181,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 +212,7 @@ namespace OpenRA.Mods.RA IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue); - cursor = ab.info.Cursor; + cursor = ab.Info.Cursor; if (negativeDamage) return false; @@ -223,7 +225,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..e48528e1cf 100644 --- a/OpenRA.Mods.RA/Attack/AttackWander.cs +++ b/OpenRA.Mods.RA/Attack/AttackWander.cs @@ -33,6 +33,7 @@ namespace OpenRA.Mods.RA public void TickIdle(Actor self) { var target = self.CenterPosition + new WVec(0, -1024*Info.MoveRadius, 0).Rotate(WRot.FromFacing(self.World.SharedRandom.Next(255))); + // TODO: This needs to be looked into again. The bigger MoveRadius is, the bigger chance that the selected coordinates will be invalid. self.Trait().ResolveOrder(self, new Order("AttackMove", self, false) { TargetLocation = self.World.Map.CellContaining(target) }); } } diff --git a/OpenRA.Mods.RA/AutoTarget.cs b/OpenRA.Mods.RA/AutoTarget.cs index 4b887faec2..151a02a9c7 100644 --- a/OpenRA.Mods.RA/AutoTarget.cs +++ b/OpenRA.Mods.RA/AutoTarget.cs @@ -10,6 +10,7 @@ using System.Drawing; using System.Linq; +using OpenRA.Mods.RA.Move; using OpenRA.Traits; namespace OpenRA.Mods.RA @@ -163,12 +164,15 @@ namespace OpenRA.Mods.RA nextScanTime = self.World.SharedRandom.Next(info.MinimumScanTimeInterval, info.MaximumScanTimeInterval); var inRange = self.World.FindActorsInCircle(self.CenterPosition, range); + var mobile = self.TraitOrDefault(); return inRange .Where(a => - a.AppearsHostileTo(self) && - !a.HasTrait() && - attack.HasAnyValidWeapons(Target.FromActor(a)) && - self.Owner.Shroud.IsTargetable(a)) + a.AppearsHostileTo(self) && + !a.HasTrait() && + attack.HasAnyValidWeapons(Target.FromActor(a)) && + self.Owner.Shroud.IsTargetable(a) && + (!attack.Info.AttackRequiresEnteringCell || (mobile != null && mobile.MovementSpeedForCell(self, a.Location) != 0)) + ) .ClosestTo(self); } } 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/maps/shellmap/map.yaml b/mods/d2k/maps/shellmap/map.yaml index affdc50af0..c0c57039ed 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: WormSpawnLocation + Location: 46,64 + Owner: Creeps Smudges: 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..9c2ae3560c --- /dev/null +++ b/mods/d2k/rules/arrakis.yaml @@ -0,0 +1,63 @@ +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 + Rock: 100 # TEMP + TargetableUnit: + TargetTypes: Underground + RevealsShroud: + Range: 32c0 + RenderUnit: + BodyOrientation: + BelowUnits: + HiddenUnderFog: + Sandworm: + WanderMoveRadius: 10 + SelectionDecorations: # TEMP + Selectable: # TEMP + Voice: WormVoice # TEMP + AppearsOnRadar: + UseLocation: yes + AttackSwallow: + AttackRequiresEnteringCell: TRUE + AttackMove: + AutoTarget: + ScanRadius: 32 + Armament: + Weapon: WormJaw + +WormSpawnLocation: + Immobile: + WormSpawner: \ No newline at end of file diff --git a/mods/d2k/rules/misc.yaml b/mods/d2k/rules/misc.yaml index 12257edc36..42ae5e839c 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 diff --git a/mods/d2k/rules/world.yaml b/mods/d2k/rules/world.yaml index 26dd5e7d75..7568cfd118 100644 --- a/mods/d2k/rules/world.yaml +++ b/mods/d2k/rules/world.yaml @@ -10,6 +10,8 @@ World: ScreenShaker: BuildingInfluence: ChooseBuildTabOnSelect: + WormManager: + SpawnInterval: 10 CrateSpawner: Minimum: 0 Maximum: 2 diff --git a/mods/d2k/sequences/infantry.yaml b/mods/d2k/sequences/infantry.yaml index 8c64824a8e..5aff4af1d3 100644 --- a/mods/d2k/sequences/infantry.yaml +++ b/mods/d2k/sequences/infantry.yaml @@ -441,4 +441,20 @@ 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 + icon: wormicon + Start: 0 \ No newline at end of file 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