Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
332
OpenRA.Mods.Common/Traits/Player/BulkProductionQueue.cs
Normal file
332
OpenRA.Mods.Common/Traits/Player/BulkProductionQueue.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
#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.Collections.Immutable;
|
||||
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 ImmutableArray<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.RefundResources(actorData.Resources);
|
||||
playerResources.RefundCash(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.RefundResources(actor.Resources);
|
||||
playerResources.RefundCash(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--;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user