Files
OpenRA/OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs
let5sne.win10 9d80847a9a
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
ra: 托管时镜头自动跟随防守热点
新增 BotTakeoverCameraFollower(Player trait):托管开启时通过 ViewportCenterProvider 自动跟随热点事件,默认偏防守(基地/矿区受击优先,建造落点次之),并使用热点衰减 + 最短驻留时间 + 切换阈值避免多处战斗时镜头抖动。

同时:

- mods/ra/rules/player.yaml 接入该 trait(仅 takeover 激活时生效)

- 修复 ingame-player.yaml 的 TooltipDesc/FTL 校验警告

- 小幅代码风格优化(不改行为)
2026-01-11 23:27:24 +08:00

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));
}
}
}