Compare commits
3 Commits
master
...
feature/ll
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d00ffa28f3 | ||
|
|
9d80847a9a | ||
|
|
ec3107e6e7 |
501
OpenRA.Mods.Common/Traits/Player/BotTakeoverCameraFollower.cs
Normal file
501
OpenRA.Mods.Common/Traits/Player/BotTakeoverCameraFollower.cs
Normal file
@@ -0,0 +1,501 @@
|
||||
#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.Graphics;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[TraitLocation(SystemActors.Player)]
|
||||
[Desc("When the local human player is handed over to a classic bot, automatically follow important defensive events with the camera.")]
|
||||
public sealed class BotTakeoverCameraFollowerInfo : TraitInfo
|
||||
{
|
||||
[Desc("Enable camera following behavior.")]
|
||||
public readonly bool Enabled = true;
|
||||
|
||||
[Desc("Only follow while the BotTakeoverManager is active.")]
|
||||
public readonly bool RequireTakeoverActive = true;
|
||||
|
||||
[Desc("How often (in ticks) to re-evaluate the best focus point.")]
|
||||
public readonly int ReevaluateIntervalTicks = 5;
|
||||
|
||||
[Desc("Minimum time (in milliseconds) to stay on a focus point before switching, unless a much higher priority event appears.")]
|
||||
public readonly int MinFocusDurationMs = 2500;
|
||||
|
||||
[Desc("Before MinFocusDurationMs expires, the new focus must have at least this multiple of the current focus score to switch.")]
|
||||
public readonly float LockedSwitchScoreMultiplier = 1.35f;
|
||||
|
||||
[Desc("Forget hotspots after this time (in milliseconds).")]
|
||||
public readonly int HotspotLifetimeMs = 15000;
|
||||
|
||||
[Desc("Merge nearby hotspots within this distance (in WPos units).")]
|
||||
public readonly int HotspotMergeDistance = 2048;
|
||||
|
||||
[Desc("Maximum number of hotspots to track.")]
|
||||
public readonly int MaxHotspots = 12;
|
||||
|
||||
[Desc("Camera move speed (in WPos units per second) when smoothly tracking within a focus point.")]
|
||||
public readonly float CameraSpeed = 60000f;
|
||||
|
||||
[Desc("Snap instantly when the focus point changes.")]
|
||||
public readonly bool SnapOnFocusChange = true;
|
||||
|
||||
[Desc("Weight when a base building (construction yard / MCV) is attacked.")]
|
||||
public readonly float BaseBuildingAttackedWeight = 120f;
|
||||
|
||||
[Desc("Weight when a harvester is attacked.")]
|
||||
public readonly float HarvesterAttackedWeight = 110f;
|
||||
|
||||
[Desc("Weight when a refinery is attacked.")]
|
||||
public readonly float RefineryAttackedWeight = 105f;
|
||||
|
||||
[Desc("Weight when another structure is attacked.")]
|
||||
public readonly float StructureAttackedWeight = 80f;
|
||||
|
||||
[Desc("Weight when a unit is attacked.")]
|
||||
public readonly float UnitAttackedWeight = 45f;
|
||||
|
||||
[Desc("Weight when a building is placed (used as a low-priority 'expansion' focus when not under attack).")]
|
||||
public readonly float BuildingPlacedWeight = 22f;
|
||||
|
||||
public override object Create(ActorInitializer init) { return new BotTakeoverCameraFollower(init.Self, this); }
|
||||
}
|
||||
|
||||
public sealed class BotTakeoverCameraFollower : ITick, IWorldLoaded, INotifyDamage, INotifyBuildingPlaced
|
||||
{
|
||||
enum HotspotKind { Damage, Build }
|
||||
|
||||
sealed class Hotspot
|
||||
{
|
||||
public float2 Position;
|
||||
public float Strength;
|
||||
public long LastUpdateTime;
|
||||
public HotspotKind Kind;
|
||||
}
|
||||
|
||||
readonly Actor playerActor;
|
||||
readonly World world;
|
||||
readonly BotTakeoverCameraFollowerInfo info;
|
||||
|
||||
WorldRenderer worldRenderer;
|
||||
Func<float2> previousCenterProvider;
|
||||
bool centerProviderInstalled;
|
||||
|
||||
readonly List<Hotspot> hotspots = [];
|
||||
|
||||
bool lastFollowActive;
|
||||
int reevaluateCountdown;
|
||||
|
||||
Hotspot currentFocus;
|
||||
long currentFocusStartTime;
|
||||
|
||||
float2 targetPos;
|
||||
bool hasTarget;
|
||||
bool snapToTarget;
|
||||
|
||||
float2 cameraPos;
|
||||
bool hasCameraPos;
|
||||
long lastCameraUpdateTime;
|
||||
|
||||
public BotTakeoverCameraFollower(Actor self, BotTakeoverCameraFollowerInfo info)
|
||||
{
|
||||
playerActor = self;
|
||||
world = self.World;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr)
|
||||
{
|
||||
worldRenderer = wr;
|
||||
}
|
||||
|
||||
void ITick.Tick(Actor self)
|
||||
{
|
||||
if (!info.Enabled || worldRenderer == null)
|
||||
return;
|
||||
|
||||
var followActive = IsFollowActive();
|
||||
if (followActive)
|
||||
EnsureCenterProviderInstalled();
|
||||
else
|
||||
EnsureCenterProviderUninstalled();
|
||||
|
||||
if (followActive != lastFollowActive)
|
||||
{
|
||||
lastFollowActive = followActive;
|
||||
OnFollowActiveChanged(followActive);
|
||||
}
|
||||
|
||||
if (!followActive)
|
||||
return;
|
||||
|
||||
if (--reevaluateCountdown > 0)
|
||||
return;
|
||||
|
||||
reevaluateCountdown = Math.Max(1, info.ReevaluateIntervalTicks);
|
||||
|
||||
PruneHotspots();
|
||||
UpdateTarget();
|
||||
}
|
||||
|
||||
void INotifyDamage.Damaged(Actor damaged, AttackInfo e)
|
||||
{
|
||||
if (!info.Enabled || worldRenderer == null)
|
||||
return;
|
||||
|
||||
if (!IsLocalPlayablePlayer())
|
||||
return;
|
||||
|
||||
if (damaged == null || damaged.IsDead || !damaged.IsInWorld)
|
||||
return;
|
||||
|
||||
if (e.Attacker == null)
|
||||
return;
|
||||
|
||||
if (e.Attacker.Owner == damaged.Owner)
|
||||
return;
|
||||
|
||||
if (e.Attacker == damaged.World.WorldActor)
|
||||
return;
|
||||
|
||||
// Don't track healing / resurrect.
|
||||
if (e.Damage.Value <= 0)
|
||||
return;
|
||||
|
||||
AddHotspot(damaged.CenterPosition, GetDamageWeight(damaged), HotspotKind.Damage);
|
||||
}
|
||||
|
||||
void INotifyBuildingPlaced.BuildingPlaced(Actor self, Actor building)
|
||||
{
|
||||
if (!info.Enabled || worldRenderer == null)
|
||||
return;
|
||||
|
||||
if (!IsLocalPlayablePlayer())
|
||||
return;
|
||||
|
||||
if (building == null || building.IsDead || !building.IsInWorld)
|
||||
return;
|
||||
|
||||
AddHotspot(building.CenterPosition, info.BuildingPlacedWeight, HotspotKind.Build);
|
||||
}
|
||||
|
||||
bool IsLocalPlayablePlayer()
|
||||
{
|
||||
var localPlayer = world.LocalPlayer;
|
||||
if (localPlayer == null || localPlayer.Spectating)
|
||||
return false;
|
||||
|
||||
return playerActor.Owner == localPlayer;
|
||||
}
|
||||
|
||||
bool IsFollowActive()
|
||||
{
|
||||
if (!IsLocalPlayablePlayer())
|
||||
return false;
|
||||
|
||||
if (!info.RequireTakeoverActive)
|
||||
return true;
|
||||
|
||||
var takeover = playerActor.TraitOrDefault<BotTakeoverManager>();
|
||||
return takeover != null && takeover.IsActive;
|
||||
}
|
||||
|
||||
void EnsureCenterProviderInstalled()
|
||||
{
|
||||
if (centerProviderInstalled)
|
||||
return;
|
||||
|
||||
if (!IsLocalPlayablePlayer())
|
||||
return;
|
||||
|
||||
previousCenterProvider = worldRenderer.Viewport.ViewportCenterProvider;
|
||||
worldRenderer.Viewport.ViewportCenterProvider = ViewportCenterProvider;
|
||||
centerProviderInstalled = true;
|
||||
}
|
||||
|
||||
void EnsureCenterProviderUninstalled()
|
||||
{
|
||||
if (!centerProviderInstalled)
|
||||
return;
|
||||
|
||||
worldRenderer.Viewport.ViewportCenterProvider = previousCenterProvider;
|
||||
previousCenterProvider = null;
|
||||
centerProviderInstalled = false;
|
||||
}
|
||||
|
||||
float2 ViewportCenterProvider()
|
||||
{
|
||||
if (worldRenderer == null)
|
||||
return float2.Zero;
|
||||
|
||||
var now = Game.RunTime;
|
||||
var viewportCenter = new float2(worldRenderer.Viewport.CenterPosition.X, worldRenderer.Viewport.CenterPosition.Y);
|
||||
|
||||
if (!IsFollowActive())
|
||||
{
|
||||
lastCameraUpdateTime = now;
|
||||
cameraPos = viewportCenter;
|
||||
hasCameraPos = true;
|
||||
return viewportCenter;
|
||||
}
|
||||
|
||||
if (!hasCameraPos)
|
||||
{
|
||||
lastCameraUpdateTime = now;
|
||||
cameraPos = viewportCenter;
|
||||
hasCameraPos = true;
|
||||
}
|
||||
|
||||
var dtMs = Math.Max(0, now - lastCameraUpdateTime);
|
||||
lastCameraUpdateTime = now;
|
||||
|
||||
if (!hasTarget)
|
||||
return viewportCenter;
|
||||
|
||||
if (snapToTarget)
|
||||
{
|
||||
snapToTarget = false;
|
||||
cameraPos = targetPos;
|
||||
return cameraPos;
|
||||
}
|
||||
|
||||
// Prevent huge jumps when the framerate is very low or the game is paused.
|
||||
var dt = Math.Min(dtMs / 1000f, 0.25f);
|
||||
var maxStep = info.CameraSpeed * dt;
|
||||
|
||||
var delta = targetPos - cameraPos;
|
||||
var dist = delta.Length;
|
||||
if (dist <= 0.001f || dist <= maxStep)
|
||||
cameraPos = targetPos;
|
||||
else
|
||||
cameraPos += delta * (maxStep / dist);
|
||||
|
||||
return cameraPos;
|
||||
}
|
||||
|
||||
void OnFollowActiveChanged(bool active)
|
||||
{
|
||||
reevaluateCountdown = 0;
|
||||
currentFocus = null;
|
||||
currentFocusStartTime = 0;
|
||||
hasTarget = false;
|
||||
snapToTarget = false;
|
||||
hasCameraPos = false;
|
||||
lastCameraUpdateTime = 0;
|
||||
|
||||
if (!active)
|
||||
{
|
||||
hotspots.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
hotspots.Clear();
|
||||
targetPos = GetBaseFocusOrViewport();
|
||||
hasTarget = true;
|
||||
snapToTarget = info.SnapOnFocusChange;
|
||||
}
|
||||
|
||||
float2 GetBaseFocusOrViewport()
|
||||
{
|
||||
var baseActor = world.ActorsHavingTrait<BaseBuilding>()
|
||||
.FirstOrDefault(a => !a.IsDead && a.IsInWorld && a.Owner == playerActor.Owner);
|
||||
|
||||
if (baseActor != null)
|
||||
return new float2(baseActor.CenterPosition.X, baseActor.CenterPosition.Y);
|
||||
|
||||
return new float2(worldRenderer.Viewport.CenterPosition.X, worldRenderer.Viewport.CenterPosition.Y);
|
||||
}
|
||||
|
||||
float GetDamageWeight(Actor damaged)
|
||||
{
|
||||
if (damaged.Info.HasTraitInfo<BaseBuildingInfo>())
|
||||
return info.BaseBuildingAttackedWeight;
|
||||
|
||||
if (damaged.Info.HasTraitInfo<HarvesterInfo>())
|
||||
return info.HarvesterAttackedWeight;
|
||||
|
||||
if (damaged.Info.HasTraitInfo<RefineryInfo>())
|
||||
return info.RefineryAttackedWeight;
|
||||
|
||||
if (damaged.Info.HasTraitInfo<BuildingInfo>())
|
||||
return info.StructureAttackedWeight;
|
||||
|
||||
return info.UnitAttackedWeight;
|
||||
}
|
||||
|
||||
void AddHotspot(WPos pos, float weight, HotspotKind kind)
|
||||
{
|
||||
if (weight <= 0)
|
||||
return;
|
||||
|
||||
var now = Game.RunTime;
|
||||
var p = new float2(pos.X, pos.Y);
|
||||
|
||||
var mergeDist2 = info.HotspotMergeDistance * (long)info.HotspotMergeDistance;
|
||||
|
||||
Hotspot best = null;
|
||||
var bestDist2 = long.MaxValue;
|
||||
|
||||
for (var i = 0; i < hotspots.Count; i++)
|
||||
{
|
||||
var h = hotspots[i];
|
||||
var d = p - h.Position;
|
||||
var dist2 = (long)(d.X * d.X + d.Y * d.Y);
|
||||
if (dist2 <= mergeDist2 && dist2 < bestDist2)
|
||||
{
|
||||
best = h;
|
||||
bestDist2 = dist2;
|
||||
}
|
||||
}
|
||||
|
||||
if (best != null)
|
||||
{
|
||||
var total = best.Strength + weight;
|
||||
if (total <= 0.001f)
|
||||
best.Position = p;
|
||||
else
|
||||
best.Position = (best.Position * best.Strength + p * weight) / total;
|
||||
|
||||
best.Strength = total;
|
||||
best.LastUpdateTime = now;
|
||||
if (kind == HotspotKind.Damage)
|
||||
best.Kind = HotspotKind.Damage;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hotspots.Count >= info.MaxHotspots)
|
||||
RemoveLowestScoreHotspot(now);
|
||||
|
||||
hotspots.Add(new Hotspot
|
||||
{
|
||||
Position = p,
|
||||
Strength = weight,
|
||||
LastUpdateTime = now,
|
||||
Kind = kind
|
||||
});
|
||||
}
|
||||
|
||||
void RemoveLowestScoreHotspot(long now)
|
||||
{
|
||||
var worstIndex = -1;
|
||||
var worstScore = float.MaxValue;
|
||||
|
||||
for (var i = 0; i < hotspots.Count; i++)
|
||||
{
|
||||
var score = GetHotspotScore(hotspots[i], now);
|
||||
if (score < worstScore)
|
||||
{
|
||||
worstScore = score;
|
||||
worstIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (worstIndex >= 0)
|
||||
hotspots.RemoveAt(worstIndex);
|
||||
}
|
||||
|
||||
void PruneHotspots()
|
||||
{
|
||||
var now = Game.RunTime;
|
||||
|
||||
for (var i = hotspots.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var age = now - hotspots[i].LastUpdateTime;
|
||||
if (age > info.HotspotLifetimeMs)
|
||||
{
|
||||
if (ReferenceEquals(hotspots[i], currentFocus))
|
||||
currentFocus = null;
|
||||
hotspots.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float GetHotspotScore(Hotspot h, long now)
|
||||
{
|
||||
var age = now - h.LastUpdateTime;
|
||||
if (age <= 0)
|
||||
return h.Strength;
|
||||
|
||||
if (age >= info.HotspotLifetimeMs)
|
||||
return 0;
|
||||
|
||||
var decay = 1f - (float)age / info.HotspotLifetimeMs;
|
||||
return h.Strength * decay;
|
||||
}
|
||||
|
||||
void UpdateTarget()
|
||||
{
|
||||
var now = Game.RunTime;
|
||||
|
||||
Hotspot best = null;
|
||||
var bestScore = 0f;
|
||||
|
||||
for (var i = 0; i < hotspots.Count; i++)
|
||||
{
|
||||
var h = hotspots[i];
|
||||
var score = GetHotspotScore(h, now);
|
||||
if (score <= 0)
|
||||
continue;
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
best = h;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
var focusScore = currentFocus != null ? GetHotspotScore(currentFocus, now) : 0f;
|
||||
if (focusScore <= 0)
|
||||
currentFocus = null;
|
||||
|
||||
var want = best;
|
||||
if (want == null || bestScore <= 0)
|
||||
{
|
||||
currentFocus = null;
|
||||
targetPos = GetBaseFocusOrViewport();
|
||||
hasTarget = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentFocus == null)
|
||||
{
|
||||
SwitchFocus(want, now);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(currentFocus, want))
|
||||
{
|
||||
var locked = now < currentFocusStartTime + info.MinFocusDurationMs;
|
||||
if (!locked)
|
||||
SwitchFocus(want, now);
|
||||
else if (bestScore >= focusScore * info.LockedSwitchScoreMultiplier)
|
||||
SwitchFocus(want, now);
|
||||
}
|
||||
|
||||
targetPos = currentFocus.Position;
|
||||
hasTarget = true;
|
||||
}
|
||||
|
||||
void SwitchFocus(Hotspot newFocus, long now)
|
||||
{
|
||||
currentFocus = newFocus;
|
||||
currentFocusStartTime = now;
|
||||
targetPos = newFocus.Position;
|
||||
hasTarget = true;
|
||||
|
||||
if (info.SnapOnFocusChange)
|
||||
snapToTarget = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
196
OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs
Normal file
196
OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
#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));
|
||||
}
|
||||
}
|
||||
}
|
||||
56
OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs
Normal file
56
OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
#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 OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Widgets;
|
||||
|
||||
namespace OpenRA.Mods.Common.Widgets
|
||||
{
|
||||
public class LLMTakeoverLogic : ChromeLogic
|
||||
{
|
||||
readonly World world;
|
||||
|
||||
[ObjectCreator.UseCtor]
|
||||
public LLMTakeoverLogic(Widget widget, World world, Dictionary<string, MiniYaml> logicArgs)
|
||||
{
|
||||
this.world = world;
|
||||
|
||||
var button = widget.Get<ButtonWidget>("LLM_TAKEOVER");
|
||||
button.GetText = () => IsTakeoverActive() ? "Cancel AI" : "AI";
|
||||
button.IsHighlighted = IsTakeoverActive;
|
||||
button.OnClick = OnTakeoverClick;
|
||||
button.IsDisabled = () => world.LocalPlayer == null
|
||||
|| world.LocalPlayer.WinState != WinState.Undefined;
|
||||
}
|
||||
|
||||
bool IsTakeoverActive()
|
||||
{
|
||||
var player = world.LocalPlayer;
|
||||
if (player == null)
|
||||
return false;
|
||||
|
||||
var takeover = player.PlayerActor.TraitOrDefault<BotTakeoverManager>();
|
||||
return takeover != null && takeover.IsActive;
|
||||
}
|
||||
|
||||
void OnTakeoverClick()
|
||||
{
|
||||
var player = world.LocalPlayer;
|
||||
if (player == null)
|
||||
return;
|
||||
|
||||
// Prefer the classic AI takeover manager (uses ModularBot + original bot modules).
|
||||
var takeover = player.PlayerActor.TraitOrDefault<BotTakeoverManager>();
|
||||
takeover?.Toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
103
README.md
103
README.md
@@ -1,60 +1,83 @@
|
||||
# OpenRA
|
||||
# OpenRA(中文)
|
||||
|
||||
A Libre/Free Real Time Strategy game engine supporting early Westwood classics.
|
||||
OpenRA 是一个自由/开源的即时战略(RTS)游戏引擎,支持早期 Westwood 经典作品的重制与扩展。
|
||||
|
||||
* Website: [https://www.openra.net](https://www.openra.net)
|
||||
* Chat: [#openra on Libera](ircs://irc.libera.chat:6697/openra) ([web](https://web.libera.chat/#openra)) or [Discord](https://discord.openra.net) 
|
||||
* Repository: [https://github.com/OpenRA/OpenRA](https://github.com/OpenRA/OpenRA) 
|
||||
- 官网:https://www.openra.net
|
||||
- 交流:Libera 上的 `#openra`([web](https://web.libera.chat/#openra))或 [Discord](https://discord.openra.net)
|
||||
- 上游仓库:https://github.com/OpenRA/OpenRA
|
||||
|
||||
Please read the [FAQ](https://github.com/OpenRA/OpenRA/wiki/FAQ) in our [Wiki](https://github.com/OpenRA/OpenRA/wiki) and report problems at [https://github.com/OpenRA/OpenRA/issues](https://github.com/OpenRA/OpenRA/issues).
|
||||
请先阅读 Wiki 中的 [FAQ](https://github.com/OpenRA/OpenRA/wiki/FAQ) 与开发文档,并在上游 Issues 反馈引擎问题:https://github.com/OpenRA/OpenRA/issues
|
||||
|
||||
Join the [Forum](https://forum.openra.net/) for discussion.
|
||||
也可加入论坛讨论:https://forum.openra.net/
|
||||
|
||||
## Play
|
||||
## 本仓库/本分支的关键实现记录
|
||||
|
||||
Distributed mods include a reimagining of
|
||||
本分支围绕“开局将本地人类玩家控制权交还给传统人机(Classic Bot)并让比赛以人机 vs 人机方式运转”,以及“托管时镜头自动跟随防守热点”做了增强。
|
||||
|
||||
* Command & Conquer: Red Alert
|
||||
* Command & Conquer: Tiberian Dawn
|
||||
* Dune 2000
|
||||
- 开局自动托管给传统人机:`OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs`
|
||||
- 作为 Player Trait,在 `WorldLoaded` 后为“本地人类玩家(LocalPlayer)”自动激活经典 `ModularBot(normal)`。
|
||||
- 仅在主机侧生效,并避免在回放中启用(防止非权威端/回放污染世界状态)。
|
||||
- 当前在 `mods/ra/rules/player.yaml` 默认开启:`BotTakeoverManager: AutoActivate: true`。
|
||||
- 托管时镜头自动跟随(默认偏“防守保家”):`OpenRA.Mods.Common/Traits/Player/BotTakeoverCameraFollower.cs`
|
||||
- 通过 `INotifyDamage`/`INotifyBuildingPlaced` 收集“热点”(受击位置、建造落点等),按权重+时间衰减选取最佳焦点。
|
||||
- 默认优先级:基地/MCV/矿车/矿厂受击 > 其它建筑受击 > 单位受击 > 新建筑落点。
|
||||
- 多处战斗会使用“最短驻留时间 + 切换阈值”避免镜头频繁抖动;切换焦点可选择瞬移或限速平滑移动。
|
||||
- 当前在 `mods/ra/rules/player.yaml` 默认开启,并且只在托管激活时生效(`RequireTakeoverActive: true`)。
|
||||
- 游戏内托管开关按钮(用于取消/恢复托管):
|
||||
- UI:`mods/ra/chrome/ingame-player.yaml`(`Button@LLM_TAKEOVER`)
|
||||
- 逻辑:`OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs`
|
||||
|
||||
EA has not endorsed and does not support this product.
|
||||
## 游玩
|
||||
|
||||
Check our [Playing the Game](https://github.com/OpenRA/OpenRA/wiki/Playing-the-game) Guide to win multiplayer matches.
|
||||
发行的内置模组包含对以下经典 RTS 的重制:
|
||||
|
||||
## Contribute
|
||||
- Command & Conquer: Red Alert(红色警戒)
|
||||
- Command & Conquer: Tiberian Dawn(泰伯利亚黎明)
|
||||
- Dune 2000(沙丘 2000)
|
||||
|
||||
* Please read [INSTALL.md](https://github.com/OpenRA/OpenRA/blob/bleed/INSTALL.md) and [Compiling](https://github.com/OpenRA/OpenRA/wiki/Compiling) on how to set up an OpenRA development environment.
|
||||
* See [Hacking](https://github.com/OpenRA/OpenRA/wiki/Hacking) for a (now very outdated) overview of the engine.
|
||||
* Read and follow our [Code of Conduct](https://github.com/OpenRA/OpenRA/blob/bleed/CODE_OF_CONDUCT.md).
|
||||
* To get your patches merged, please adhere to the [Contributing](https://github.com/OpenRA/OpenRA/blob/bleed/CONTRIBUTING.md) guidelines.
|
||||
EA 未背书且不支持本产品。
|
||||
|
||||
## Mapping
|
||||
多人对战入门可参考:[Playing the Game](https://github.com/OpenRA/OpenRA/wiki/Playing-the-game)
|
||||
|
||||
* We offer a [Mapping](https://github.com/OpenRA/OpenRA/wiki/Mapping) Tutorial as you can change gameplay drastically with custom rules.
|
||||
* For scripted mission have a look at the [Lua API](https://docs.openra.net/en/release/lua/).
|
||||
* If you want to share your maps with the community, upload them at the [OpenRA Resource Center](https://resource.openra.net).
|
||||
## 编译与运行(Windows)
|
||||
|
||||
## Modding
|
||||
- 编译(推荐):`.\make.cmd all`
|
||||
- 快速启动红色警戒模组(本仓库提供脚本):`.\start-ra.cmd`
|
||||
|
||||
* Download a copy of the [OpenRA Mod SDK](https://github.com/OpenRA/OpenRAModSDK) to start your own mod.
|
||||
* Check the [Modding Guide](https://github.com/OpenRA/OpenRA/wiki/Modding-Guide) to create your own classic RTS.
|
||||
* There exists an auto-generated [Trait documentation](https://docs.openra.net/en/latest/release/traits/) to get started with yaml files.
|
||||
* Some hints on how to create new OpenRA compatible [Pixelart](https://github.com/OpenRA/OpenRA/wiki/Pixelart).
|
||||
* Upload total conversions at [our Mod DB profile](https://www.moddb.com/games/openra/mods).
|
||||
更多开发环境配置请阅读:
|
||||
- [INSTALL.md](https://github.com/OpenRA/OpenRA/blob/bleed/INSTALL.md)
|
||||
- Wiki: [Compiling](https://github.com/OpenRA/OpenRA/wiki/Compiling)
|
||||
|
||||
## Support
|
||||
## 贡献
|
||||
|
||||
* Sponsor a [mirror server](https://github.com/OpenRA/OpenRAWebsiteV3/tree/master/packages) if you have some bandwidth to spare.
|
||||
* You can immediately set up a [Dedicated](https://github.com/OpenRA/OpenRA/wiki/Dedicated-Server) Game Server.
|
||||
- 请阅读并遵守:[Code of Conduct](https://github.com/OpenRA/OpenRA/blob/bleed/CODE_OF_CONDUCT.md)
|
||||
- 提交补丁请遵循:[CONTRIBUTING.md](https://github.com/OpenRA/OpenRA/blob/bleed/CONTRIBUTING.md)
|
||||
- 引擎架构概览(可能已过时):[Hacking](https://github.com/OpenRA/OpenRA/wiki/Hacking)
|
||||
|
||||
## 地图制作(Mapping)
|
||||
|
||||
- 教程:[Mapping](https://github.com/OpenRA/OpenRA/wiki/Mapping)
|
||||
- Lua 脚本任务相关:[Lua API](https://docs.openra.net/en/release/lua/)
|
||||
- 分享地图:OpenRA Resource Center:https://resource.openra.net
|
||||
|
||||
## 模组开发(Modding)
|
||||
|
||||
- Mod SDK:https://github.com/OpenRA/OpenRAModSDK
|
||||
- 指南:[Modding Guide](https://github.com/OpenRA/OpenRA/wiki/Modding-Guide)
|
||||
- Traits 文档(自动生成):https://docs.openra.net/en/latest/release/traits/
|
||||
- 像素美术提示:[Pixelart](https://github.com/OpenRA/OpenRA/wiki/Pixelart)
|
||||
- 发布大型模组(Total Conversion):https://www.moddb.com/games/openra/mods
|
||||
|
||||
## 支持
|
||||
|
||||
- 赞助镜像下载服务器: https://github.com/OpenRA/OpenRAWebsiteV3/tree/master/packages
|
||||
- 搭建专用服务器:[Dedicated Server](https://github.com/OpenRA/OpenRA/wiki/Dedicated-Server)
|
||||
|
||||
## 许可证
|
||||
|
||||
## License
|
||||
Copyright (c) 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](https://github.com/OpenRA/OpenRA/blob/bleed/COPYING).
|
||||
OpenRA 使用 GNU GPLv3(或更高版本)授权发布,详见 [COPYING](https://github.com/OpenRA/OpenRA/blob/bleed/COPYING)。
|
||||
|
||||
# Sponsors
|
||||
Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
|
||||
## 赞助方
|
||||
|
||||
Windows 平台的免费代码签名由 [SignPath.io](https://about.signpath.io/) 提供,证书来自 [SignPath Foundation](https://signpath.org/)。
|
||||
|
||||
@@ -285,6 +285,20 @@ Container@PLAYER_WIDGETS:
|
||||
Y: 5
|
||||
ImageCollection: stance-icons
|
||||
ImageName: hold-fire
|
||||
Container@LLM_TAKEOVER_BAR:
|
||||
Logic: LLMTakeoverLogic
|
||||
X: 440
|
||||
Y: WINDOW_HEIGHT - HEIGHT - 14
|
||||
Width: 80
|
||||
Height: 26
|
||||
Children:
|
||||
Button@LLM_TAKEOVER:
|
||||
Width: 80
|
||||
Height: 26
|
||||
Font: Bold
|
||||
TooltipText: button-llm-takeover.tooltip
|
||||
TooltipDesc: button-llm-takeover.tooltipdesc
|
||||
TooltipContainer: TOOLTIP_CONTAINER
|
||||
Container@MUTE_INDICATOR:
|
||||
Logic: MuteIndicatorLogic
|
||||
X: WINDOW_WIDTH - WIDTH - 260
|
||||
|
||||
@@ -190,3 +190,13 @@ button-production-types-aircraft-tooltip = 飞机
|
||||
button-production-types-naval-tooltip = 海军
|
||||
button-production-types-scroll-up-tooltip = 向上滚动
|
||||
button-production-types-scroll-down-tooltip = 向下滚动
|
||||
|
||||
## LLM Takeover
|
||||
button-llm-takeover =
|
||||
.tooltip = AI托管
|
||||
.tooltipdesc =
|
||||
启用AI托管模式(默认使用传统人机AI)。
|
||||
AI将自动控制你的所有单位和建筑,
|
||||
包括建造、生产、战斗和资源管理。
|
||||
|
||||
再次点击可取消托管,恢复手动控制。
|
||||
|
||||
@@ -185,3 +185,11 @@ Player:
|
||||
PlayerExperience:
|
||||
GameSaveViewportManager:
|
||||
PlayerRadarTerrain:
|
||||
BotTakeoverManager:
|
||||
AutoActivate: true
|
||||
AutoActivateDelayTicks: 1
|
||||
BotType: normal
|
||||
GrantConditions: enable-normal-ai
|
||||
BotTakeoverCameraFollower:
|
||||
Enabled: true
|
||||
RequireTakeoverActive: true
|
||||
|
||||
Reference in New Issue
Block a user