diff --git a/AUTHORS b/AUTHORS index 89832c2abd..b84fa41b0a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -139,6 +139,7 @@ Also thanks to: * Teemu Nieminen (Temeez) * Tim Mylemans (gecko) * Tirili + * Tomas Einarsson (Mesacer) * Tristan Keating (Kilkakon) * Tristan Mühlbacher (MicroBit) * UnknownProgrammer diff --git a/OpenRA.Game/Graphics/HardwareCursor.cs b/OpenRA.Game/Graphics/HardwareCursor.cs index 5675b76942..1d044bc3e5 100644 --- a/OpenRA.Game/Graphics/HardwareCursor.cs +++ b/OpenRA.Game/Graphics/HardwareCursor.cs @@ -125,6 +125,8 @@ namespace OpenRA.Graphics public void Render(Renderer renderer) { } + public int Frame { get { return frame; } } + public void Dispose() { foreach (var cursors in hardwareCursors) diff --git a/OpenRA.Game/Graphics/SoftwareCursor.cs b/OpenRA.Game/Graphics/SoftwareCursor.cs index 3bdd04a61a..acdd2dd0e6 100644 --- a/OpenRA.Game/Graphics/SoftwareCursor.cs +++ b/OpenRA.Game/Graphics/SoftwareCursor.cs @@ -21,6 +21,7 @@ namespace OpenRA.Graphics void Render(Renderer renderer); void SetCursor(string cursor); void Tick(); + int Frame { get; } } public sealed class SoftwareCursor : ICursor @@ -77,7 +78,7 @@ namespace OpenRA.Graphics return; var cursorSequence = cursorProvider.GetCursorSequence(cursorName); - var cursorSprite = sprites[cursorName][((int)cursorFrame % cursorSequence.Length)]; + var cursorSprite = sprites[cursorName][Frame]; var cursorSize = CursorProvider.CursorViewportZoomed ? 2.0f * cursorSprite.Size : cursorSprite.Size; var cursorOffset = CursorProvider.CursorViewportZoomed ? @@ -91,6 +92,15 @@ namespace OpenRA.Graphics cursorSize); } + public int Frame + { + get + { + var cursorSequence = cursorProvider.GetCursorSequence(cursorName); + return (int)cursorFrame % cursorSequence.Length; + } + } + public void Dispose() { palette.Dispose(); diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index ec202ae64f..c040e71a61 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -331,6 +331,7 @@ + diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs index f43e674b14..5da7332710 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs @@ -36,6 +36,12 @@ namespace OpenRA.Mods.Common.Traits [Desc("Amount of time to keep the camera alive after the aircraft have finished attacking")] public readonly int CameraRemoveDelay = 25; + [Desc("Placeholder cursor animation for the target cursor when the real cursor is invisible.")] + public readonly string TargetPlaceholderCursorAnimation = null; + + [Desc("Animation used to render the direction arrows.")] + public readonly string DirectionArrowAnimation = null; + [Desc("Weapon range offset to apply during the beacon clock calculation")] public readonly WDist BeaconDistanceOffset = WDist.FromCells(6); @@ -52,11 +58,19 @@ namespace OpenRA.Mods.Common.Traits this.info = info; } + public override void SelectTarget(Actor self, string order, SupportPowerManager manager) + { + Game.Sound.PlayToPlayer(SoundType.UI, manager.Self.Owner, Info.SelectTargetSound); + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", + Info.SelectTargetSpeechNotification, self.Owner.Faction.InternalName); + + self.World.OrderGenerator = new SelectDirectionalTarget(self.World, order, manager, Info.Cursor, info.TargetPlaceholderCursorAnimation, info.DirectionArrowAnimation); + } + public override void Activate(Actor self, Order order, SupportPowerManager manager) { base.Activate(self, order, manager); - - SendAirstrike(self, order.Target.CenterPosition); + SendAirstrike(self, order.Target.CenterPosition, order.ExtraData == uint.MaxValue, (int)order.ExtraData); } public void SendAirstrike(Actor self, WPos target, bool randomize = true, int attackFacing = 0) diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/ParatroopersPower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/ParatroopersPower.cs index 27fcedd1a3..b462ab15de 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/ParatroopersPower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/ParatroopersPower.cs @@ -50,6 +50,12 @@ namespace OpenRA.Mods.Common.Traits [Desc("Amount of time (in ticks) to keep the camera alive while the passengers drop.")] public readonly int CameraRemoveDelay = 85; + [Desc("Placeholder cursor animation for the target cursor when the real cursor is invisible.")] + public readonly string TargetPlaceholderCursorAnimation = null; + + [Desc("Animation used to render the direction arrows.")] + public readonly string DirectionArrowAnimation = null; + [Desc("Weapon range offset to apply during the beacon clock calculation.")] public readonly WDist BeaconDistanceOffset = WDist.FromCells(4); @@ -66,11 +72,20 @@ namespace OpenRA.Mods.Common.Traits this.info = info; } + public override void SelectTarget(Actor self, string order, SupportPowerManager manager) + { + Game.Sound.PlayToPlayer(SoundType.UI, manager.Self.Owner, Info.SelectTargetSound); + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", + Info.SelectTargetSpeechNotification, self.Owner.Faction.InternalName); + + self.World.OrderGenerator = new SelectDirectionalTarget(self.World, order, manager, Info.Cursor, info.TargetPlaceholderCursorAnimation, info.DirectionArrowAnimation); + } + public override void Activate(Actor self, Order order, SupportPowerManager manager) { base.Activate(self, order, manager); - SendParatroopers(self, order.Target.CenterPosition); + SendParatroopers(self, order.Target.CenterPosition, order.ExtraData == uint.MaxValue, (int)order.ExtraData); } public Actor[] SendParatroopers(Actor self, WPos target, bool randomize = true, int dropFacing = 0) diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/SelectDirectionalTarget.cs b/OpenRA.Mods.Common/Traits/SupportPowers/SelectDirectionalTarget.cs new file mode 100644 index 0000000000..4895fd2bfb --- /dev/null +++ b/OpenRA.Mods.Common/Traits/SupportPowers/SelectDirectionalTarget.cs @@ -0,0 +1,186 @@ +#region Copyright & License Information +/* + * Copyright 2007-2019 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public class SelectDirectionalTarget : IOrderGenerator + { + readonly string order; + readonly SupportPowerManager manager; + readonly string cursor; + readonly Animation targetCursor; + + readonly string[] arrows = { "arrow-t", "arrow-tl", "arrow-l", "arrow-bl", "arrow-b", "arrow-br", "arrow-r", "arrow-tr" }; + readonly Arrow[] directionArrows; + + CPos targetCell; + int2 location; + int2 dragLocation; + bool beginDrag; + bool dragStarted; + Arrow currentArrow; + + public SelectDirectionalTarget(World world, string order, SupportPowerManager manager, string cursor, string targetPlaceholderCursorAnimation, + string directionArrowAnimation) + { + this.order = order; + this.manager = manager; + this.cursor = cursor; + + targetCursor = new Animation(world, targetPlaceholderCursorAnimation); + targetCursor.PlayRepeating("cursor"); + + for (var i = 0; i < Game.Cursor.Frame; i++) + targetCursor.Tick(); + + directionArrows = LoadArrows(directionArrowAnimation, world, arrows.Length); + } + + IEnumerable IOrderGenerator.Order(World world, CPos cell, int2 worldPixel, MouseInput mi) + { + if (mi.Button == MouseButton.Right) + { + world.CancelInputMode(); + yield break; + } + + if (mi.Button == MouseButton.Left && mi.Event == MouseInputEvent.Down) + { + if (!beginDrag) + { + targetCell = cell; + location = mi.Location; + beginDrag = true; + } + + yield break; + } + + if (mi.Event == MouseInputEvent.Move) + { + if (beginDrag) + { + dragLocation = mi.Location; + var angle = AngleBetween(location, dragLocation); + currentArrow = GetArrow(angle); + dragStarted = true; + + yield break; + } + } + + if (mi.Button == MouseButton.Left && mi.Event == MouseInputEvent.Up) + { + yield return new Order(order, manager.Self, Target.FromCell(manager.Self.World, targetCell), false) + { + SuppressVisualFeedback = true, + ExtraData = IsOutsideDragZone ? (uint)currentArrow.Direction.Facing : uint.MaxValue + }; + + world.CancelInputMode(); + } + } + + void IOrderGenerator.Tick(World world) + { + targetCursor.Tick(); + + // Cancel the OG if we can't use the power + if (!manager.Powers.ContainsKey(order)) + world.CancelInputMode(); + } + + bool IsOutsideDragZone + { + get { return dragStarted && (dragLocation - location).Length > 20; } + } + + IEnumerable IOrderGenerator.Render(WorldRenderer wr, World world) { yield break; } + + IEnumerable IOrderGenerator.RenderAboveShroud(WorldRenderer wr, World world) + { + if (!beginDrag) + return Enumerable.Empty(); + + var palette = wr.Palette("chrome"); + var worldPx = wr.Viewport.ViewToWorldPx(location); + var worldPos = wr.ProjectedPosition(worldPx); + var renderables = new List(targetCursor.Render(worldPos, WVec.Zero, -511, palette, 1 / wr.Viewport.Zoom)); + + if (IsOutsideDragZone) + renderables.Add(new SpriteRenderable(currentArrow.Sprite, worldPos, WVec.Zero, -511, palette, 1 / wr.Viewport.Zoom, true)); + + return renderables; + } + + string IOrderGenerator.GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi) { return beginDrag ? "invisible" : cursor; } + + // Starting at (0, -1) and rotating in CCW + static double AngleBetween(int2 p1, int2 p2) + { + var radian = Math.Atan2(p2.Y - p1.Y, p2.X - p1.X); + var d = radian * (180 / Math.PI); + if (d < 0.0) + d += 360.0; + var angle = 270.0 - d; + if (angle < 0) + angle += 360.0; + + return angle; + } + + Arrow GetArrow(double degree) + { + var arrow = directionArrows.FirstOrDefault(d => d.EndAngle >= degree); + return arrow ?? directionArrows[0]; + } + + Arrow[] LoadArrows(string cursorAnimation, World world, int noOfDividingPoints) + { + var points = new Arrow[noOfDividingPoints]; + var partAngle = 360 / noOfDividingPoints; + var i1 = partAngle / 2d; + + for (var i = 0; i < noOfDividingPoints; i++) + { + var sprite = world.Map.Rules.Sequences.GetSequence(cursorAnimation, arrows[i]).GetSprite(0); + + var angle = i * partAngle; + var direction = WAngle.FromDegrees(angle); + var endAngle = angle + i1; + + points[i] = new Arrow(sprite, endAngle, direction); + } + + return points; + } + + class Arrow + { + public Sprite Sprite { get; private set; } + public double EndAngle { get; private set; } + public WAngle Direction { get; private set; } + + public Arrow(Sprite sprite, double endAngle, WAngle direction) + { + Sprite = sprite; + EndAngle = endAngle; + Direction = direction; + } + } + } +} diff --git a/mods/cnc/cursors.yaml b/mods/cnc/cursors.yaml index 7c28d0924e..985826d2e3 100644 --- a/mods/cnc/cursors.yaml +++ b/mods/cnc/cursors.yaml @@ -136,6 +136,8 @@ Cursors: sell-vehicle: Start: 154 Length: 24 + invisible: + Start: 28 mouse3.shp: cursor default: Start: 0 diff --git a/mods/cnc/rules/structures.yaml b/mods/cnc/rules/structures.yaml index a2b2003184..1469a853a1 100644 --- a/mods/cnc/rules/structures.yaml +++ b/mods/cnc/rules/structures.yaml @@ -628,6 +628,8 @@ HQ: ArrowSequence: arrow ClockSequence: clock CircleSequence: circles + TargetPlaceholderCursorAnimation: airstriketarget + DirectionArrowAnimation: airstrikedirection SupportPowerChargeBar: Power: Amount: -50 diff --git a/mods/cnc/sequences/misc.yaml b/mods/cnc/sequences/misc.yaml index d5a9d0400f..a6a99c06f1 100644 --- a/mods/cnc/sequences/misc.yaml +++ b/mods/cnc/sequences/misc.yaml @@ -436,3 +436,47 @@ smokland: Length: 32 Tick: 120 ZOffset: 1023 + +airstriketarget: + cursor: mouse2 + Start: 88 + Length: 8 + Tick: 80 + +airstrikedirection: + arrow-t: mouse2 + Start: 1 + Y: -12 + Offset: 0, -15, 0 + arrow-tr: mouse2 + Start: 2 + X: 14 + Y: -12 + Offset: 7, -7, 0 + arrow-r: mouse2 + Start: 3 + X: 14 + Offset: 12, 0, 0 + arrow-br: mouse2 + Start: 4 + X: 14 + Y: 11 + Offset: 7, 7, 0 + arrow-b: mouse2 + Start: 5 + Y: 11 + Offset: 0, 15, 0 + arrow-bl: mouse2 + Start: 6 + X: -15 + Y: 11 + Offset: -7, 7, 0 + arrow-l: mouse2 + Start: 7 + X: -15 + Offset: -12, 0, 0 + arrow-tl: mouse2 + Start: 8 + X: -15 + Y: -12 + Offset: -7, -7, 0 diff --git a/mods/ra/chrome/ingame-player.yaml b/mods/ra/chrome/ingame-player.yaml index 6dabfd3705..477a377221 100644 --- a/mods/ra/chrome/ingame-player.yaml +++ b/mods/ra/chrome/ingame-player.yaml @@ -8,14 +8,6 @@ Container@PLAYER_WIDGETS: X: 10 Y: 10 Children: - SupportPowers@SUPPORT_PALETTE: - IconSize: 62, 46 - IconSpriteOffset: -1, -1 - TooltipContainer: TOOLTIP_CONTAINER - ReadyText: READY - HoldText: ON HOLD - HotkeyPrefix: SupportPower - HotkeyCount: 6 Container@PALETTE_FOREGROUND: Children: Image@ICON_TEMPLATE: @@ -24,9 +16,18 @@ Container@PLAYER_WIDGETS: Y: 0 - 2 Width: 62 Height: 46 + ClickThrough: false IgnoreMouseOver: true ImageCollection: sidebar ImageName: background-supportoverlay + SupportPowers@SUPPORT_PALETTE: + IconSize: 62, 46 + IconSpriteOffset: -1, -1 + TooltipContainer: TOOLTIP_CONTAINER + ReadyText: READY + HoldText: ON HOLD + HotkeyPrefix: SupportPower + HotkeyCount: 6 SupportPowerTimer@SUPPORT_POWER_TIMER: X: 80 Y: 10 diff --git a/mods/ra/cursors.yaml b/mods/ra/cursors.yaml index 83734569e6..865ae30e91 100644 --- a/mods/ra/cursors.yaml +++ b/mods/ra/cursors.yaml @@ -200,6 +200,8 @@ Cursors: sell2: Start: 148 Length: 12 + invisible: + Start: 34 nopower.shp: cursor powerdown-blocked: Start: 0 diff --git a/mods/ra/rules/structures.yaml b/mods/ra/rules/structures.yaml index 68d0bcf6a3..bfa176abae 100644 --- a/mods/ra/rules/structures.yaml +++ b/mods/ra/rules/structures.yaml @@ -1461,6 +1461,8 @@ AFLD: ArrowSequence: arrow ClockSequence: clock CircleSequence: circles + TargetPlaceholderCursorAnimation: paratarget + DirectionArrowAnimation: paradirection ParatroopersPower@paratroopers: OrderName: SovietParatroopers Prerequisites: aircraft.soviet @@ -1479,6 +1481,8 @@ AFLD: ArrowSequence: arrow ClockSequence: clock CircleSequence: circles + TargetPlaceholderCursorAnimation: paratarget + DirectionArrowAnimation: paradirection AirstrikePower@parabombs: OrderName: UkraineParabombs Prerequisites: aircraft.ukraine @@ -1498,6 +1502,8 @@ AFLD: ArrowSequence: arrow ClockSequence: clock CircleSequence: circles + TargetPlaceholderCursorAnimation: paratarget + DirectionArrowAnimation: paradirection ProductionBar: ProductionType: Aircraft SupportPowerChargeBar: diff --git a/mods/ra/sequences/misc.yaml b/mods/ra/sequences/misc.yaml index def2628fd7..253e5d3c20 100644 --- a/mods/ra/sequences/misc.yaml +++ b/mods/ra/sequences/misc.yaml @@ -627,3 +627,47 @@ ctflag: bib: mbGAP Length: * UseTilesetExtension: true + +paratarget: + cursor: mouse + Start: 82 + Length: 8 + Tick: 80 + +paradirection: + arrow-t: mouse + Start: 1 + Y: -7 + Offset: 0, -19, 0 + arrow-tr: mouse + Start: 2 + X: 6 + Y: -5 + Offset: 15, -15, 0 + arrow-r: mouse + Start: 3 + X: 7 + Offset: 19, 0, 0 + arrow-br: mouse + Start: 4 + X: 6 + Y: 5 + Offset: 15, 15, 0 + arrow-b: mouse + Start: 5 + Y: 7 + Offset: 0, 19, 0 + arrow-bl: mouse + Start: 6 + X: -6 + Y: 5 + Offset: -15, 15, 0 + arrow-l: mouse + Start: 7 + X: -8 + Offset: -19, 0, 0 + arrow-tl: mouse + Start: 8 + X: -6 + y: 5 + Offset: -15, -15, 0