D2k Starport

- OG fly path and Frigate behaviour
- BulkProductionQueue
- ProductionStarport trait
- OG palette widget behavior for Bulk queue
- Purchase button for each BulkProduction
- Delivery timer for each BulkProduction
This commit is contained in:
N.N
2024-05-02 13:07:34 +02:00
committed by Gustas Kažukauskas
parent fcae6a26d1
commit 18870cbb03
17 changed files with 825 additions and 54 deletions

View File

@@ -0,0 +1,118 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.Collections.Generic;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
public class DeliverBulkOrder : Activity
{
readonly Actor producer;
readonly List<(ActorInfo Actor, int Resources, int Cash)> orderedActors;
readonly string productionType;
readonly BulkProductionQueue queue;
readonly Cargo cargo;
int delayBetweenUnloads = 0;
public DeliverBulkOrder(Actor transport, Actor producer, List<(ActorInfo Actor, int Resources, int Cash)> orderedActors,
string productionType, BulkProductionQueue queue)
{
this.producer = producer;
this.orderedActors = orderedActors;
this.productionType = productionType;
this.queue = queue;
cargo = transport.Trait<Cargo>();
}
protected override void OnFirstRun(Actor self)
{
var landingOffset = producer.Info.TraitInfo<ProductionBulkAirdropInfo>().LandOffset;
QueueChild(new Land(self, Target.FromActor(producer), WDist.FromCells(0), landingOffset));
if (cargo.Info.BeforeUnloadDelay > 0)
QueueChild(new Wait(cargo.Info.BeforeUnloadDelay));
}
protected override void OnLastRun(Actor self)
{
if (!producer.IsDead || producer.IsInWorld)
foreach (var cargo in producer.TraitsImplementing<INotifyDelivery>())
cargo.Delivered(producer);
if (cargo.Info.AfterUnloadDelay > 0)
Queue(new Wait(cargo.Info.AfterUnloadDelay));
Queue(new FlyOffMap(self, Target.FromCell(self.World, self.World.Map.ChooseClosestEdgeCell(self.Location))));
Queue(new RemoveSelf());
}
protected override void OnActorDispose(Actor self)
{
queue.DeliverFinished();
}
public override bool Tick(Actor self)
{
if (!producer.IsInWorld || producer.IsDead)
{
// Try to find another ProductionBulkAirDrop
var newProducer = self.World.ActorsHavingTrait<ProductionBulkAirdrop>()
.Where(a => a.Owner == self.Owner)
.ClosestToIgnoringPath(self);
if (newProducer != null)
{
Cancel(self);
Queue(new DeliverBulkOrder(self, newProducer, orderedActors, productionType, queue));
return true;
}
else
{
queue.DeliverFinished();
return true;
}
}
if (orderedActors == null || orderedActors.Count == 0)
{
queue.DeliverFinished();
return true;
}
var actor = orderedActors[^1];
var productionTrait = producer.Trait<ProductionBulkAirdrop>();
var exit = productionTrait.PublicExit(producer, actor.Actor, productionType);
if (exit == null)
return false;
if (delayBetweenUnloads > 0)
{
delayBetweenUnloads--;
return false;
}
delayBetweenUnloads = cargo.Info.BetweenUnloadDelay;
producer.World.AddFrameEndTask(ww =>
{
var inits = new TypeDictionary
{
new OwnerInit(self.Owner),
new FactionInit(BuildableInfo.GetInitialFaction(actor.Actor, producer.Trait<ProductionBulkAirdrop>().Faction))
};
productionTrait.DoProduction(producer, actor.Actor, exit?.Info, productionType, inits);
orderedActors.Remove(actor);
});
return false;
}
}
}

View File

@@ -27,6 +27,7 @@ namespace OpenRA.Mods.Common.Activities
readonly Mobile mobile;
readonly bool assignTargetOnFirstRun;
readonly WDist unloadRange;
int delayBetweenUnloads = 0;
Target destination;
bool takeOffAfterUnload;
@@ -96,6 +97,13 @@ namespace OpenRA.Mods.Common.Activities
if (cargo.CanUnload())
{
if (delayBetweenUnloads > 0)
{
delayBetweenUnloads--;
return false;
}
delayBetweenUnloads = cargo.Info.BetweenUnloadDelay;
foreach (var inu in notifiers)
inu.Unloading(self);

View File

@@ -59,6 +59,9 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Delay (in ticks) before continuing after unloading a passenger.")]
public readonly int AfterUnloadDelay = 25;
[Desc("Delay (in ticks) before each passenger is unloaded.")]
public readonly int BetweenUnloadDelay = 0;
[CursorReference]
[Desc("Cursor to display when able to unload the passengers.")]
public readonly string UnloadCursor = "deploy";

View File

@@ -0,0 +1,331 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.Player)]
[Desc("Attach this to the player actor (not a building!) to define a new shared build queue.",
"Allows you to build multiple actors before delivery",
"Will work together with the `ProductionBulkAirDrop` trait on the actor that actually does the production.",
"You will also want to add `PrimaryBuilding` to let the user choose where new units should exit.")]
public class BulkProductionQueueInfo : ProductionQueueInfo, Requires<TechTreeInfo>, Requires<PlayerResourcesInfo>
{
[Desc("Maximum order capacity.")]
public readonly int MaxCapacity = 6;
[Desc("Delivery delay in ticks.")]
public readonly int DeliveryDelay = 1500;
[NotificationReference("Speech")]
[Desc("Notification played when the delivery started.")]
public readonly string StartDeliveryNotification = null;
[Desc("Return funds if the delivery fails.")]
public readonly bool RefundUndeliveredActors = true;
[FluentReference(optional: true)]
[Desc("Notification displayed when the delivery started.")]
public readonly string StartDeliveryTextNotification = null;
[NotificationReference("Speech")]
[Desc("Notifications to play when the delivery actor is on its way. Last notification is played when the actors to deliver are spawned on the map.")]
public readonly string[] DeliveryProgressNotifications = [];
public override object Create(ActorInitializer init) { return new BulkProductionQueue(init, this); }
}
public class BulkProductionQueue : ProductionQueue
{
static readonly ActorInfo[] NoItems = [];
readonly Actor self;
readonly BulkProductionQueueInfo info;
protected readonly List<(ActorInfo Actor, int Resources, int Cash)> ActorsReadyForDelivery = [];
public int DeliveryDelay { get; private set; } = 0;
protected int notificationInterval = 0;
protected int notificationIndex = 0;
protected bool deliveryProcessStarted = false;
public BulkProductionQueue(ActorInitializer init, BulkProductionQueueInfo info)
: base(init, info)
{
self = init.Self;
this.info = info;
if (info.DeliveryProgressNotifications.Length != 0)
notificationInterval = info.DeliveryDelay / info.DeliveryProgressNotifications.Length;
}
protected override void Tick(Actor self)
{
// PERF: Avoid LINQ.
Enabled = false;
var isActive = false;
foreach (var x in self.World.ActorsWithTrait<Production>())
{
if (x.Trait.IsTraitDisabled)
continue;
if (x.Actor.Owner != self.Owner || !x.Trait.Info.Produces.Contains(Info.Type))
continue;
Enabled |= IsValidFaction;
isActive |= !x.Trait.IsTraitPaused;
}
if (!Enabled)
{
DeliverFinished();
ClearQueue();
}
else
{
if (HasDeliveryStarted() && DeliveryDelay > 0)
{
DeliveryDelay--;
PlayDeliveryProgressNotifications();
}
else if (HasDeliveryStarted() && DeliveryDelay == 0)
{
PlayDeliveryProgressNotifications();
DeliveryHasArrived();
DeliveryDelay--;
}
}
TickInner(self, !isActive);
}
public override IEnumerable<ActorInfo> AllItems()
{
return Enabled ? base.AllItems() : NoItems;
}
public override IEnumerable<ActorInfo> BuildableItems()
{
return Enabled && ActorsReadyForDelivery.Count != info.MaxCapacity && !deliveryProcessStarted ? base.BuildableItems() : NoItems;
}
public override TraitPair<Production> MostLikelyProducer()
{
var productionActor = self.World.ActorsWithTrait<Production>()
.Where(x => x.Actor.Owner == self.Owner
&& !x.Trait.IsTraitDisabled && x.Trait.Info.Produces.Contains(Info.Type))
.OrderBy(x => x.Trait.IsTraitPaused)
.ThenByDescending(x => x.Actor.Trait<PrimaryBuilding>().IsPrimary)
.ThenByDescending(x => x.Actor.ActorID)
.FirstOrDefault();
return productionActor;
}
protected override bool BuildUnit(ActorInfo unit)
{
// Find a production structure to build this actor
var bi = unit.TraitInfo<BuildableInfo>();
// Some units may request a specific production type, which is ignored if the AllTech cheat is enabled
var type = developerMode.AllTech ? Info.Type : (bi.BuildAtProductionType ?? Info.Type);
var producers = self.World.ActorsWithTrait<Production>()
.Where(x => x.Actor.Owner == self.Owner
&& !x.Trait.IsTraitDisabled
&& x.Trait.Info.Produces.Contains(type))
.OrderByDescending(x => x.Actor.Trait<PrimaryBuilding>().IsPrimary)
.OrderByDescending(x => x.Actor.Trait<PrimaryBuilding>().IsPrimary)
.ThenByDescending(x => x.Actor.ActorID);
var anyProducers = false;
foreach (var p in producers)
{
anyProducers = true;
if (p.Trait.IsTraitPaused)
continue;
if (ActorsReadyForDelivery.Count <= info.MaxCapacity)
{
var item = Queue.First(i => i.Done && i.Item == unit.Name);
ActorsReadyForDelivery.Add((unit, item.ResourcesPaid, item.TotalCost - item.ResourcesPaid));
EndProduction(item);
return true;
}
}
if (!anyProducers)
CancelProduction(unit.Name, 1);
return false;
}
public override void ResolveOrder(Actor self, Order order)
{
if (!Enabled)
return;
var rules = self.World.Map.Rules;
switch (order.OrderString)
{
case "StartProduction":
var unit = rules.Actors[order.TargetString];
var bi = unit.TraitInfo<BuildableInfo>();
// Not built by this queue
if (!bi.Queue.Contains(Info.Type))
return;
// You can't build that
if (BuildableItems().All(b => b.Name != order.TargetString))
return;
// Check if the player is trying to build more units that they are allowed
var fromLimit = int.MaxValue;
if (!developerMode.AllTech)
{
if (Info.QueueLimit > 0)
fromLimit = Info.QueueLimit - Queue.Count;
if (Info.ItemLimit > 0)
fromLimit = Math.Min(fromLimit, Info.ItemLimit - Queue.Count(i => i.Item == order.TargetString));
if (bi.BuildLimit > 0)
{
var inQueue = Queue.Count(pi => pi.Item == order.TargetString);
var owned = self.Owner.World.ActorsHavingTrait<Buildable>().Count(a => a.Info.Name == order.TargetString && a.Owner == self.Owner);
fromLimit = Math.Min(fromLimit, bi.BuildLimit - (inQueue + owned));
}
if (fromLimit <= 0)
return;
}
var cost = GetProductionCost(unit);
var time = GetBuildTime(unit, bi);
var amountToBuild = Math.Min(fromLimit, order.ExtraData);
for (var n = 0; n < amountToBuild; n++)
{
if (Info.PayUpFront && cost > playerResources.GetCashAndResources())
return;
BeginProduction(new ProductionItem(this, order.TargetString, cost, playerPower, () => self.World.AddFrameEndTask(_ =>
{
// Make sure the item hasn't been invalidated between the ProductionItem ticking and this FrameEndTask running
if (!Queue.Any(i => i.Done && i.Item == unit.Name))
return;
BuildUnit(unit);
})), !order.Queued);
}
break;
case "PauseProduction":
PauseProduction(order.TargetString, order.ExtraData != 0);
break;
case "CancelProduction":
CancelProduction(order.TargetString, order.ExtraData);
break;
case "ReturnOrder":
ReturnOrder(order.TargetString, order.ExtraData);
break;
case "PurchaseOrder":
if (!deliveryProcessStarted && order.TargetString == info.Type)
StartDeliveryProcess();
break;
}
}
public void DeliverFinished()
{
if (info.RefundUndeliveredActors || !deliveryProcessStarted)
ReturnOrder();
ActorsReadyForDelivery.Clear();
deliveryProcessStarted = false;
}
public bool HasDeliveryStarted()
{
return deliveryProcessStarted;
}
public List<(ActorInfo Actor, int Resources, int Cash)> GetActorsReadyForDelivery()
{
return ActorsReadyForDelivery;
}
protected void StartDeliveryProcess()
{
ClearQueue();
deliveryProcessStarted = true;
DeliveryDelay = info.DeliveryDelay;
var rules = self.World.Map.Rules;
Game.Sound.PlayNotification(rules, self.Owner, "Speech", info.StartDeliveryNotification, self.Owner.Faction.InternalName);
if (info.StartDeliveryTextNotification != null)
TextNotificationsManager.AddTransientLine(self.Owner, info.StartDeliveryTextNotification);
}
protected void DeliveryHasArrived()
{
var producers = self.World.ActorsWithTrait<ProductionBulkAirdrop>()
.Where(x => x.Actor.Owner == self.Owner
&& !x.Trait.IsTraitDisabled
&& !x.Trait.IsTraitPaused
&& x.Trait.Info.Produces.Contains(Info.Type))
.OrderByDescending(x => x.Actor.Trait<PrimaryBuilding>().IsPrimary)
.ThenByDescending(x => x.Actor.ActorID);
var p = producers.FirstOrDefault();
p.Trait?.DeliverOrder(p.Actor, ActorsReadyForDelivery, Info.Type, this);
}
public void ReturnOrder(string itemName = null, uint numberToCancel = 1)
{
if (itemName == null)
{
foreach (var actorData in ActorsReadyForDelivery)
{
playerResources.GiveResources(actorData.Resources);
playerResources.GiveCash(actorData.Cash);
}
ActorsReadyForDelivery.Clear();
return;
}
for (var i = 0; i < numberToCancel; i++)
{
var actor = ActorsReadyForDelivery.LastOrDefault(actor => actor.Actor.Name == itemName);
if (actor.Actor == null)
break;
playerResources.GiveResources(actor.Resources);
playerResources.GiveCash(actor.Cash);
ActorsReadyForDelivery.Remove(actor);
}
}
protected void PlayDeliveryProgressNotifications()
{
if (notificationInterval == 0)
{
Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech",
info.DeliveryProgressNotifications[notificationIndex], self.Owner.Faction.InternalName);
notificationInterval = info.DeliveryDelay / info.DeliveryProgressNotifications.Length;
notificationIndex++;
if (info.DeliveryProgressNotifications.Length == notificationIndex)
{
notificationIndex = 0;
return;
}
}
notificationInterval--;
}
}
}

View File

@@ -145,7 +145,7 @@ namespace OpenRA.Mods.Common.Traits
protected Production[] productionTraits;
// Will change if the owner changes
PowerManager playerPower;
protected PowerManager playerPower;
protected PlayerResources playerResources;
protected DeveloperMode developerMode;
protected TechTree techTree;
@@ -435,7 +435,7 @@ namespace OpenRA.Mods.Common.Traits
return true;
}
public void ResolveOrder(Actor self, Order order)
public virtual void ResolveOrder(Actor self, Order order)
{
if (!Enabled)
return;

View File

@@ -0,0 +1,141 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.Activities;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[Desc("Deliver multiple units via skylift. Works with BulkProductionQueue")]
public class ProductionBulkAirdropInfo : ProductionInfo
{
[NotificationReference("Speech")]
[Desc("Speech notification to play when a unit is delivered.")]
public readonly string ReadyNotification = "Reinforce";
[FluentReference(optional: true)]
[Desc("Text notification to display when a unit is delivered.")]
public readonly string ReadyTextNotification = null;
[FieldLoader.Require]
[ActorReference(typeof(AircraftInfo))]
[Desc("Cargo aircraft used for delivery. Must have the `" + nameof(Aircraft) + "` trait.")]
public readonly string ActorType = null;
[Desc("Direction the aircraft should face to land.")]
public readonly WAngle Facing = new(256);
[Desc("Offset the aircraft used for landing.")]
public readonly WVec LandOffset = WVec.Zero;
public override object Create(ActorInitializer init) { return new ProductionBulkAirdrop(init, this); }
}
sealed class ProductionBulkAirdrop : Production
{
RallyPoint rp;
public ProductionBulkAirdrop(ActorInitializer init, ProductionBulkAirdropInfo info)
: base(init, info) { }
protected override void Created(Actor self)
{
base.Created(self);
rp = self.TraitOrDefault<RallyPoint>();
}
public bool DeliverOrder(Actor producer, List<(ActorInfo Actor, int Resources, int Cash)> orderedActors, string productionType, BulkProductionQueue queue)
{
if (IsTraitDisabled || IsTraitPaused)
return false;
var info = (ProductionBulkAirdropInfo)Info;
var owner = producer.Owner;
var map = owner.World.Map;
var startPos = producer.World.Map.ChooseClosestEdgeCell(producer.Location);
var spawnFacing = producer.World.Map.FacingBetween(startPos, producer.Location, WAngle.Zero);
foreach (var tower in producer.TraitsImplementing<INotifyDelivery>())
tower.IncomingDelivery(producer);
owner.World.AddFrameEndTask(w =>
{
if (!producer.IsInWorld || producer.IsDead)
{
// Try to find another producer
var anotherProducer = queue.MostLikelyProducer();
if (anotherProducer.Trait != null && anotherProducer.Trait is ProductionBulkAirdrop anotherStarport)
anotherStarport.DeliverOrder(anotherProducer.Actor, orderedActors, productionType, queue);
else
queue.DeliverFinished();
return;
}
var waitTickbeforeSpawn = 15;
var aircraftInfo = producer.World.Map.Rules.Actors[info.ActorType].TraitInfo<AircraftInfo>();
var exit = producer.RandomExitOrDefault(producer.World, productionType);
var exitCell = producer.Location;
if (exit != null)
exitCell = producer.Location + exit.Info.ExitCell;
var destinations = rp != null && rp.Path.Count > 0 ? rp.Path : [exitCell];
// Aircraft fly in under their own power.
foreach (var orderedAircraft in orderedActors.Where(actor => actor.Actor.HasTraitInfo<AircraftInfo>()))
{
var altitude = orderedAircraft.Actor.TraitInfo<AircraftInfo>().CruiseAltitude;
var aircraft = w.CreateActor(orderedAircraft.Actor.Name,
[
new CenterPositionInit(w.Map.CenterOfCell(startPos) + new WVec(WDist.Zero, WDist.Zero, altitude)),
new OwnerInit(owner),
new FacingInit(spawnFacing)
]);
var move = aircraft.TraitOrDefault<IMove>();
if (move != null)
{
aircraft.QueueActivity(new Wait(waitTickbeforeSpawn));
waitTickbeforeSpawn += 15;
// first move must be to the Producer location
aircraft.QueueActivity(move.MoveTo(exitCell, 2, evaluateNearestMovableCell: true));
foreach (var cell in destinations)
aircraft.QueueActivity(move.MoveTo(cell, 2, evaluateNearestMovableCell: true));
}
}
orderedActors.RemoveAll(actor => actor.Actor.HasTraitInfo<AircraftInfo>());
if (orderedActors.Count == 0)
{
queue.DeliverFinished();
return;
}
var transport = w.CreateActor(info.ActorType,
[
new CenterPositionInit(w.Map.CenterOfCell(startPos) + new WVec(WDist.Zero, WDist.Zero, aircraftInfo.CruiseAltitude)),
new OwnerInit(owner),
new FacingInit(spawnFacing)
]);
transport.QueueActivity(new DeliverBulkOrder(transport, producer, orderedActors, productionType, queue));
});
return true;
}
public Exit PublicExit(Actor self, ActorInfo producee, string productionType)
{
return SelectExit(self, producee, productionType);
}
}
}

View File

@@ -50,6 +50,8 @@ namespace OpenRA.Mods.Common.Widgets
public readonly int2 IconSpriteOffset = int2.Zero;
public readonly float2 QueuedOffset = new(4, 2);
public readonly float2 BulkOffset = new(4, 2);
public readonly TextAlign QueuedTextAlign = TextAlign.Left;
public readonly string ClickSound = ChromeMetrics.Get<string>("ClickSound");
@@ -363,6 +365,24 @@ namespace OpenRA.Mods.Common.Widgets
bool HandleRightClick(ProductionItem item, ProductionIcon icon, int handleCount)
{
if (CurrentQueue is BulkProductionQueue bulkProductionQueue && !bulkProductionQueue.HasDeliveryStarted())
{
var readyActors = bulkProductionQueue.GetActorsReadyForDelivery();
if (readyActors.Any(a => a.Actor.Name == icon.Name))
{
World.IssueOrder(
new Order("ReturnOrder", CurrentQueue.Actor, false)
{
ExtraData = (uint)handleCount,
TargetString = icon.Name
});
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null);
return true;
}
else
return false;
}
if (item == null)
return false;
@@ -390,17 +410,18 @@ namespace OpenRA.Mods.Common.Widgets
bool HandleMiddleClick(ProductionItem item, ProductionIcon icon, int handleCount)
{
if (item == null)
return false;
if (item != null)
{
// Directly cancel, skipping "on-hold"
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", CurrentQueue.Info.CancelledAudio, World.LocalPlayer.Faction.InternalName);
TextNotificationsManager.AddTransientLine(World.LocalPlayer, CurrentQueue.Info.CancelledTextNotification);
// Directly cancel, skipping "on-hold"
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", CurrentQueue.Info.CancelledAudio, World.LocalPlayer.Faction.InternalName);
TextNotificationsManager.AddTransientLine(World.LocalPlayer, CurrentQueue.Info.CancelledTextNotification);
World.IssueOrder(Order.CancelProduction(CurrentQueue.Actor, icon.Name, handleCount));
return true;
}
World.IssueOrder(Order.CancelProduction(CurrentQueue.Actor, icon.Name, handleCount));
return true;
return false;
}
bool HandleEvent(ProductionIcon icon, MouseButton btn, Modifiers modifiers)
@@ -568,10 +589,13 @@ namespace OpenRA.Mods.Common.Widgets
var waiting = !CurrentQueue.IsProducing(first) && !first.Done;
if (first.Done)
{
if (ReadyTextStyle == ReadyTextStyleOptions.Solid || orderManager.LocalFrameNumber * worldRenderer.World.Timestep / 360 % 2 == 0)
overlayFont.DrawTextWithContrast(ReadyText, icon.Pos + readyOffset, TextColor, Color.Black, 1);
else if (ReadyTextStyle == ReadyTextStyleOptions.AlternatingColor)
overlayFont.DrawTextWithContrast(ReadyText, icon.Pos + readyOffset, ReadyTextAltColor, Color.Black, 1);
if (CurrentQueue is not BulkProductionQueue)
{
if (ReadyTextStyle == ReadyTextStyleOptions.Solid || orderManager.LocalFrameNumber * worldRenderer.World.Timestep / 360 % 2 == 0)
overlayFont.DrawTextWithContrast(ReadyText, icon.Pos + readyOffset, TextColor, Color.Black, 1);
else if (ReadyTextStyle == ReadyTextStyleOptions.AlternatingColor)
overlayFont.DrawTextWithContrast(ReadyText, icon.Pos + readyOffset, ReadyTextAltColor, Color.Black, 1);
}
}
else if (first.Paused)
overlayFont.DrawTextWithContrast(HoldText,
@@ -603,6 +627,14 @@ namespace OpenRA.Mods.Common.Widgets
TextColor, Color.Black, 1);
}
}
if (CurrentQueue is BulkProductionQueue bulkProductionQueue)
{
var readyActors = bulkProductionQueue.GetActorsReadyForDelivery().
Count(a => a.Actor.Name == icon.Name);
overlayFont.DrawTextWithContrast(readyActors.ToString(NumberFormatInfo.CurrentInfo),
icon.Pos + BulkOffset, TextColor, Color.Black, 1);
}
}
}

View File

@@ -0,0 +1,84 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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 OpenRA.Mods.Common.Traits;
using OpenRA.Mods.Common.Widgets;
using OpenRA.Primitives;
using OpenRA.Widgets;
namespace OpenRA.Mods.D2k.Widgets.Logic
{
public class PurchaseWidgetLogic : ChromeLogic
{
[FluentReference("time")]
const string DeliveryIn = "label-deliver-in-timer";
string time = "";
readonly World world;
readonly Lazy<ProductionPaletteWidget> paletteWidget;
readonly ButtonWidget purchaseButton;
readonly LabelWidget deliveryLabel;
readonly Color textColor;
[ObjectCreator.UseCtor]
public PurchaseWidgetLogic(Widget widget, World world)
{
this.world = world;
purchaseButton = widget.Get<ButtonWidget>("PURCHASE_BUTTON");
deliveryLabel = widget.Get<LabelWidget>("DELIVERY_IN");
textColor = deliveryLabel.TextColor;
var textCache = new CachedTransform<string, string>(s => FluentProvider.GetMessage(DeliveryIn, "time", time));
deliveryLabel.GetText = () => textCache.Update(time);
paletteWidget = Exts.Lazy(() => Ui.Root.Get("PRODUCTION_PALETTE") as ProductionPaletteWidget);
purchaseButton.IsVisible = () => false;
purchaseButton.OnClick = ResolveOrder;
}
public override void Tick()
{
if (world.LocalPlayer == null)
return;
if (paletteWidget.Value.CurrentQueue is BulkProductionQueue bulkProduction)
{
purchaseButton.IsVisible = () => bulkProduction.GetActorsReadyForDelivery().
Count != 0 && !bulkProduction.HasDeliveryStarted();
}
else
{
purchaseButton.IsVisible = () => false;
}
foreach (var bulkQueue in world.LocalPlayer.PlayerActor.TraitsImplementing<BulkProductionQueue>())
{
if (bulkQueue.HasDeliveryStarted())
{
time = WidgetUtils.FormatTime(bulkQueue.DeliveryDelay, world.Timestep);
deliveryLabel.Visible = true;
if (bulkQueue.DeliveryDelay <= 0 && Game.LocalTick % 25 < 15)
deliveryLabel.TextColor = Color.White;
else
deliveryLabel.TextColor = textColor;
}
else
deliveryLabel.Visible = false;
}
}
void ResolveOrder()
{
world.IssueOrder(
new Order("PurchaseOrder", paletteWidget.Value.CurrentQueue.Actor, false)
{
TargetString = paletteWidget.Value.CurrentQueue.Info.Type
});
}
}
}

View File

@@ -17,7 +17,9 @@ sidebar:
Regions:
background-top: 0, 0, 226, 295
background-iconrow: 0, 295, 226, 48
background-bottom: 0, 343, 226, 13
background-bottom: 0, 343, 226, 28
purchase-button: 226, 281, 125, 29
sidebar-button:
Inherits: ^Chrome
@@ -115,40 +117,31 @@ production-icons:
order-icons:
Inherits: ^Chrome
Regions:
debug: 10, 357, 34, 35
debug-disabled: 10, 393, 34, 35
debug-active: 10, 430, 34, 35
guard: 49, 357, 34, 35
guard-disabled: 49, 393, 34, 35
guard-active: 49, 430, 34, 35
repair: 88, 357, 34, 35
repair-disabled: 88, 393, 34, 35
repair-active: 88, 430, 34, 35
sell: 127, 357, 34, 35
sell-disabled: 127, 393, 34, 35
sell-active: 127, 430, 34, 35
options: 165, 357, 40, 38
options-disabled: 165, 393, 40, 38
options-active: 165, 430, 40, 38
beacon: 209, 357, 36, 35
beacon-disabled: 209, 393, 36, 35
beacon-active: 209, 430, 36, 35
power: 248, 357, 34, 35
power-disabled: 248, 393, 36, 35
power-active: 248, 430, 36, 35
diplomacy: 287, 357, 36, 35
diplomacy-disabled: 287, 393, 36, 35
diplomacy-active: 287, 430, 36, 35
repair: 238, 357, 34, 35
repair-disabled: 238, 393, 34, 35
repair-active: 238, 430, 34, 35
sell: 277, 357, 34, 35
sell-disabled: 277, 393, 34, 35
sell-active: 277, 430, 34, 35
options: 315, 357, 40, 38
options-disabled: 315, 393, 40, 38
options-active: 315, 430, 40, 38
beacon: 359, 357, 36, 35
beacon-disabled: 359, 393, 36, 35
beacon-active: 359, 430, 36, 35
power: 398, 357, 34, 35
power-disabled: 398, 393, 36, 35
power-active: 398, 430, 36, 35
command-button:
Inherits: ^Chrome
PanelRegion: 331, 427, 0, 0, 34, 41, 0, 0
PanelRegion: 232, 312, 0, 0, 34, 41, 0, 0
PanelSides: Center
command-button-hover:
Inherits: command-button
command-button-pressed:
Inherits: ^Chrome
PanelRegion: 401, 427, 0, 0, 34, 41, 0, 0
PanelRegion: 301, 312, 0, 0, 34, 41, 0, 0
PanelSides: Center
command-button-highlighted:
Inherits: command-button-pressed
@@ -158,7 +151,7 @@ command-button-highlighted-pressed:
Inherits: command-button-pressed
command-button-disabled:
Inherits: ^Chrome
PanelRegion: 366, 427, 0, 0, 34, 41, 0, 0
PanelRegion: 266, 312, 0, 0, 34, 41, 0, 0
PanelSides: Center
command-button-highlighted-disabled:
Inherits: command-button-disabled
@@ -304,6 +297,33 @@ progressbar-bg:
progressbar-thumb:
Inherits: button
button-purchase:
Inherits: ^Chrome
PanelRegion: 226, 281, 0, 0, 125, 29,0 ,0
button-purchase-disabled:
Inherits: button-purchase
button-purchase-hover:
Inherits: ^Chrome
PanelRegion: 226, 252, 0, 0, 125, 29,0 ,0
button-purchase-highlighted:
Inherits: button-purchase-hover
button-purchase-highlighted-hover:
Inherits: button-purchase-hover
button-purchase-pressed:
Inherits: ^Chrome
PanelRegion: 226, 223, 0, 0, 125, 29,0 ,0
button-purchase-highlighted-pressed:
Inherits: button-purchase-pressed
button-purchase-highlighted-disabled:
Inherits: button-purchase
button:
Inherits: ^Dialog
PanelRegion: 513, 1, 2, 2, 122, 122, 2, 2

View File

@@ -486,6 +486,28 @@ Container@PLAYER_WIDGETS:
ClickThrough: false
ImageCollection: sidebar
ImageName: background-bottom
Children:
Container@PURCHASE_PANEL:
Logic: PurchaseWidgetLogic
Children:
Button@PURCHASE_BUTTON:
X: PARENT_LEFT + 65
Y: PARENT_TOP + 1
Width: 125
Height: 29
Font: Bold
Background: button-purchase
TooltipText: purchase-panel-button-tooltip
TooltipContainer: TOOLTIP_CONTAINER
Label@DELIVERY_IN:
X: PARENT_TOP + 80
Y: PARENT_LEFT + 3
TextColor: FFD091
Width: 100
Height: 22
Align: Center
Font: Bold
Text: purchase-panel-label-delivery
LogicTicker@PRODUCTION_TICKER:
ProductionPalette@PRODUCTION_PALETTE:
X: 39

View File

@@ -57,6 +57,7 @@ label-combat-stats-buildings-killed-header = B. Killed
label-combat-stats-buildings-dead-header = B. Lost
label-combat-stats-army-value-header = Army Value
label-combat-stats-vision-header = Vision
label-deliver-in-timer = DELIVERY IN: { $time }
## ingame-observer.yaml, ingame-player.yaml
label-mute-indicator = Audio Muted
@@ -179,6 +180,9 @@ productionpalette-sidebar-production-palette =
.ready = READY
.hold = ON HOLD
purchase-panel-button-tooltip = Order selected units
purchase-panel-label-delivery = DELIVERY IN:
button-production-types-building-tooltip = Buildings
button-production-types-infantry-tooltip = Infantry
button-production-types-vehicle-tooltip = Light Vehicles

View File

@@ -24,9 +24,6 @@ carryall.reinforce:
Cargo:
MaxWeight: 10
frigate:
Aircraft:
LandableTerrainTypes: Sand, Rock, Transition, Spice, SpiceSand, Dune, Concrete
barracks.harkonnen:
Inherits: barracks

View File

@@ -86,16 +86,17 @@ frigate:
Aircraft:
IdleBehavior: LeaveMap
Speed: 189
TurnSpeed: 4
TurnSpeed: 16
Repulsable: False
MaximumPitch: 20
CruiseAltitude: 2048
VTOL: true
TakeOffOnCreation: false
CanHover: true
CanSlide: true
-AppearsOnRadar:
Cargo:
MaxWeight: 20
BetweenUnloadDelay: 15
RejectsOrders:
ornithopter:

View File

@@ -63,13 +63,15 @@ Player:
CancelledAudio: Cancelled
SpeedUp: true
BuildTimeSpeedReduction: 100, 66, 50
ClassicProductionQueue@Starport:
BulkProductionQueue@Starport:
Type: Starport
DisplayOrder: 4
BuildDurationModifier: 212
QueueLimit: 6
PayUpFront: true
StartDeliveryNotification: OrderPlaced
DeliveryProgressNotifications: TMinusFive, TMinusFour, TMinusThree, TMinusTwo, TMinusOne
BlockedAudio: NoRoom
BlockedTextNotification: notification-no-room-for-new-unit
QueuedAudio: OrderPlaced
OnHoldAudio: OnHold
CancelledAudio: Cancelled
ClassicProductionQueue@Aircraft:

View File

@@ -3,6 +3,7 @@ mcv.starport:
Buildable:
Prerequisites: starport
Queue: Starport
BuildDuration: 0
Valued:
Cost: 2500
-MapEditorData:
@@ -16,6 +17,7 @@ harvester.starport:
Buildable:
Prerequisites: starport
Queue: Starport
BuildDuration: 0
Valued:
Cost: 1500
-MapEditorData:
@@ -29,6 +31,7 @@ trike.starport:
Buildable:
Prerequisites: starport
Queue: Starport
BuildDuration: 0
Valued:
Cost: 315
-MapEditorData:
@@ -42,6 +45,7 @@ quad.starport:
Buildable:
Prerequisites: starport
Queue: Starport
BuildDuration: 0
Valued:
Cost: 500
-MapEditorData:
@@ -55,6 +59,7 @@ siege_tank.starport:
Buildable:
Prerequisites: starport
Queue: Starport
BuildDuration: 0
Valued:
Cost: 1075
-MapEditorData:
@@ -68,6 +73,7 @@ missile_tank.starport:
Buildable:
Prerequisites: starport
Queue: Starport
BuildDuration: 0
Valued:
Cost: 1250
-MapEditorData:
@@ -81,6 +87,7 @@ combat_tank_a.starport:
Buildable:
Prerequisites: ~starport.atreides_combat
Queue: Starport
BuildDuration: 0
Valued:
Cost: 875
-MapEditorData:
@@ -94,6 +101,7 @@ combat_tank_h.starport:
Buildable:
Prerequisites: ~starport.harkonnen_combat
Queue: Starport
BuildDuration: 0
Valued:
Cost: 875
-MapEditorData:
@@ -107,6 +115,7 @@ combat_tank_o.starport:
Buildable:
Prerequisites: ~starport.ordos_combat
Queue: Starport
BuildDuration: 0
Valued:
Cost: 875
-MapEditorData:
@@ -120,6 +129,7 @@ carryall.starport:
Buildable:
Prerequisites: starport
Queue: Starport
BuildDuration: 0
Valued:
Cost: 1500
-MapEditorData:

View File

@@ -657,9 +657,7 @@ starport:
Exit@2:
SpawnOffset: 0,-480,0
ExitCell: 0,2
ProductionAirdrop:
WaitTickBeforeProduce: 10
WaitTickAfterProduce: 15
ProductionBulkAirdrop:
LandOffset: 0, -256, 0
Produces: Starport
ActorType: frigate

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 202 KiB