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)
|
- 官网: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) 
|
- 交流:Libera 上的 `#openra`([web](https://web.libera.chat/#openra))或 [Discord](https://discord.openra.net)
|
||||||
* Repository: [https://github.com/OpenRA/OpenRA](https://github.com/OpenRA/OpenRA) 
|
- 上游仓库: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
|
- 开局自动托管给传统人机:`OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs`
|
||||||
* Command & Conquer: Tiberian Dawn
|
- 作为 Player Trait,在 `WorldLoaded` 后为“本地人类玩家(LocalPlayer)”自动激活经典 `ModularBot(normal)`。
|
||||||
* Dune 2000
|
- 仅在主机侧生效,并避免在回放中启用(防止非权威端/回放污染世界状态)。
|
||||||
|
- 当前在 `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.
|
EA 未背书且不支持本产品。
|
||||||
* 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.
|
|
||||||
|
|
||||||
## 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.
|
## 编译与运行(Windows)
|
||||||
* 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).
|
|
||||||
|
|
||||||
## 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.
|
- [INSTALL.md](https://github.com/OpenRA/OpenRA/blob/bleed/INSTALL.md)
|
||||||
* There exists an auto-generated [Trait documentation](https://docs.openra.net/en/latest/release/traits/) to get started with yaml files.
|
- Wiki: [Compiling](https://github.com/OpenRA/OpenRA/wiki/Compiling)
|
||||||
* 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).
|
|
||||||
|
|
||||||
## Support
|
## 贡献
|
||||||
|
|
||||||
* Sponsor a [mirror server](https://github.com/OpenRA/OpenRAWebsiteV3/tree/master/packages) if you have some bandwidth to spare.
|
- 请阅读并遵守:[Code of Conduct](https://github.com/OpenRA/OpenRA/blob/bleed/CODE_OF_CONDUCT.md)
|
||||||
* You can immediately set up a [Dedicated](https://github.com/OpenRA/OpenRA/wiki/Dedicated-Server) Game Server.
|
- 提交补丁请遵循:[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
|
Copyright (c) OpenRA Developers and Contributors
|
||||||
This file is part of OpenRA, which is free software. It is made
|
OpenRA 使用 GNU GPLv3(或更高版本)授权发布,详见 [COPYING](https://github.com/OpenRA/OpenRA/blob/bleed/COPYING)。
|
||||||
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).
|
|
||||||
|
|
||||||
# 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
|
Y: 5
|
||||||
ImageCollection: stance-icons
|
ImageCollection: stance-icons
|
||||||
ImageName: hold-fire
|
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:
|
Container@MUTE_INDICATOR:
|
||||||
Logic: MuteIndicatorLogic
|
Logic: MuteIndicatorLogic
|
||||||
X: WINDOW_WIDTH - WIDTH - 260
|
X: WINDOW_WIDTH - WIDTH - 260
|
||||||
|
|||||||
@@ -190,3 +190,13 @@ button-production-types-aircraft-tooltip = 飞机
|
|||||||
button-production-types-naval-tooltip = 海军
|
button-production-types-naval-tooltip = 海军
|
||||||
button-production-types-scroll-up-tooltip = 向上滚动
|
button-production-types-scroll-up-tooltip = 向上滚动
|
||||||
button-production-types-scroll-down-tooltip = 向下滚动
|
button-production-types-scroll-down-tooltip = 向下滚动
|
||||||
|
|
||||||
|
## LLM Takeover
|
||||||
|
button-llm-takeover =
|
||||||
|
.tooltip = AI托管
|
||||||
|
.tooltipdesc =
|
||||||
|
启用AI托管模式(默认使用传统人机AI)。
|
||||||
|
AI将自动控制你的所有单位和建筑,
|
||||||
|
包括建造、生产、战斗和资源管理。
|
||||||
|
|
||||||
|
再次点击可取消托管,恢复手动控制。
|
||||||
|
|||||||
@@ -185,3 +185,11 @@ Player:
|
|||||||
PlayerExperience:
|
PlayerExperience:
|
||||||
GameSaveViewportManager:
|
GameSaveViewportManager:
|
||||||
PlayerRadarTerrain:
|
PlayerRadarTerrain:
|
||||||
|
BotTakeoverManager:
|
||||||
|
AutoActivate: true
|
||||||
|
AutoActivateDelayTicks: 1
|
||||||
|
BotType: normal
|
||||||
|
GrantConditions: enable-normal-ai
|
||||||
|
BotTakeoverCameraFollower:
|
||||||
|
Enabled: true
|
||||||
|
RequireTakeoverActive: true
|
||||||
|
|||||||
Reference in New Issue
Block a user