新增 BotTakeoverCameraFollower(Player trait):托管开启时通过 ViewportCenterProvider 自动跟随热点事件,默认偏防守(基地/矿区受击优先,建造落点次之),并使用热点衰减 + 最短驻留时间 + 切换阈值避免多处战斗时镜头抖动。 同时: - mods/ra/rules/player.yaml 接入该 trait(仅 takeover 激活时生效) - 修复 ingame-player.yaml 的 TooltipDesc/FTL 校验警告 - 小幅代码风格优化(不改行为)
197 lines
4.8 KiB
C#
197 lines
4.8 KiB
C#
#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.Graphics;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
[TraitLocation(SystemActors.Player)]
|
|
[Desc("Automatically activates a classic AI bot for the local human player so the match starts as bot-vs-bot.")]
|
|
public sealed class BotTakeoverManagerInfo : TraitInfo
|
|
{
|
|
[Desc("Automatically enable takeover when the world is loaded.")]
|
|
public readonly bool AutoActivate = false;
|
|
|
|
[Desc("Delay in ticks before activating after world load (lets other world init settle).")]
|
|
public readonly int AutoActivateDelayTicks = 1;
|
|
|
|
[Desc("Bot type to activate (e.g. normal/rush/turtle/naval). Must match an IBotInfo.Type on the Player actor.")]
|
|
public readonly string BotType = "normal";
|
|
|
|
[Desc("Conditions to grant when takeover is enabled. If empty, will infer enable-<bot>-ai for known bot types.")]
|
|
public readonly ImmutableArray<string> GrantConditions = [];
|
|
|
|
public override object Create(ActorInitializer init)
|
|
{
|
|
return new BotTakeoverManager(init.Self, this);
|
|
}
|
|
}
|
|
|
|
public sealed class BotTakeoverManager : ITick, IWorldLoaded
|
|
{
|
|
readonly Actor self;
|
|
readonly World world;
|
|
readonly BotTakeoverManagerInfo info;
|
|
|
|
IBot activeBot;
|
|
readonly List<int> conditionTokens = [];
|
|
|
|
bool pendingAutoActivate;
|
|
int autoActivateRemainingTicks;
|
|
|
|
public BotTakeoverManager(Actor self, BotTakeoverManagerInfo info)
|
|
{
|
|
this.self = self;
|
|
world = self.World;
|
|
this.info = info;
|
|
}
|
|
|
|
public bool IsActive
|
|
{
|
|
get
|
|
{
|
|
if (activeBot == null)
|
|
return false;
|
|
|
|
return activeBot switch
|
|
{
|
|
ModularBot mb => mb.IsEnabled,
|
|
_ => false
|
|
};
|
|
}
|
|
}
|
|
|
|
public void Toggle()
|
|
{
|
|
SetActive(!IsActive);
|
|
}
|
|
|
|
public void SetActive(bool active)
|
|
{
|
|
if (active)
|
|
ActivateTakeover();
|
|
else
|
|
DeactivateTakeover();
|
|
}
|
|
|
|
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr)
|
|
{
|
|
if (info.AutoActivate)
|
|
{
|
|
pendingAutoActivate = true;
|
|
autoActivateRemainingTicks = Math.Max(0, info.AutoActivateDelayTicks);
|
|
}
|
|
}
|
|
|
|
void ITick.Tick(Actor self)
|
|
{
|
|
if (!pendingAutoActivate)
|
|
return;
|
|
|
|
if (autoActivateRemainingTicks-- > 0)
|
|
return;
|
|
|
|
pendingAutoActivate = false;
|
|
ActivateTakeover();
|
|
}
|
|
|
|
bool CanTakeover()
|
|
{
|
|
// Bot logic is host-authoritative. Avoid enabling during replays or on non-host clients.
|
|
if (!Game.IsHost || world.IsReplay)
|
|
return false;
|
|
|
|
var player = self.Owner;
|
|
|
|
// If this player is already a lobby bot, then the engine will have activated it already.
|
|
if (player == null || player.IsBot)
|
|
return false;
|
|
|
|
// Only takeover the local player (avoid accidentally hijacking other human slots).
|
|
return world.LocalPlayer != null && world.LocalPlayer == player;
|
|
}
|
|
|
|
void ActivateTakeover()
|
|
{
|
|
if (!CanTakeover())
|
|
return;
|
|
|
|
if (IsActive)
|
|
return;
|
|
|
|
var botType = info.BotType ?? "";
|
|
if (string.IsNullOrWhiteSpace(botType))
|
|
botType = "normal";
|
|
|
|
var bot = self.TraitsImplementing<IBot>().FirstOrDefault(b => b.Info.Type == botType);
|
|
if (bot == null)
|
|
{
|
|
Console.WriteLine($"[Bot Takeover] No IBot found for type '{botType}'.");
|
|
return;
|
|
}
|
|
|
|
GrantTakeoverConditions(botType);
|
|
|
|
bot.Activate(self.Owner);
|
|
activeBot = bot;
|
|
Console.WriteLine($"[Bot Takeover] Activated bot type '{botType}' for player {self.Owner.PlayerName}.");
|
|
}
|
|
|
|
void DeactivateTakeover()
|
|
{
|
|
if (activeBot is ModularBot mb)
|
|
mb.IsEnabled = false;
|
|
|
|
activeBot = null;
|
|
|
|
for (var i = conditionTokens.Count - 1; i >= 0; i--)
|
|
{
|
|
var token = conditionTokens[i];
|
|
if (self.TokenValid(token))
|
|
self.RevokeCondition(token);
|
|
}
|
|
|
|
conditionTokens.Clear();
|
|
Console.WriteLine("[Bot Takeover] Deactivated.");
|
|
}
|
|
|
|
void GrantTakeoverConditions(string botType)
|
|
{
|
|
if (conditionTokens.Count > 0)
|
|
return;
|
|
|
|
var conditions = info.GrantConditions;
|
|
if (conditions.Length == 0)
|
|
{
|
|
var inferred = botType.ToLowerInvariant() switch
|
|
{
|
|
"rush" => "enable-rush-ai",
|
|
"normal" => "enable-normal-ai",
|
|
"turtle" => "enable-turtle-ai",
|
|
"naval" => "enable-naval-ai",
|
|
_ => null
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(inferred))
|
|
conditions = conditions.Add(inferred);
|
|
}
|
|
|
|
foreach (var c in conditions.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct(StringComparer.OrdinalIgnoreCase))
|
|
conditionTokens.Add(self.GrantCondition(c));
|
|
}
|
|
}
|
|
}
|