zc
This commit is contained in:
488
Source/WulaFallenEmpire/Building/Building_ExtraGraphics.cs
Normal file
488
Source/WulaFallenEmpire/Building/Building_ExtraGraphics.cs
Normal file
@@ -0,0 +1,488 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using System.Linq;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class Building_ExtraGraphics : Building
|
||||
{
|
||||
// 通过 ModExtension 配置的图形数据
|
||||
private ExtraGraphicsExtension modExtension;
|
||||
|
||||
// 图形缓存 - 现在包含Shader信息
|
||||
private Dictionary<string, Graphic> graphicsCache = new Dictionary<string, Graphic>();
|
||||
|
||||
// 动画状态 - 每个图层的独立状态
|
||||
private Dictionary<int, float> layerHoverOffsets = new Dictionary<int, float>();
|
||||
private Dictionary<int, float> layerAnimationTimes = new Dictionary<int, float>();
|
||||
private Dictionary<int, float> layerRotationAngles = new Dictionary<int, float>();
|
||||
private int lastTick = -1;
|
||||
|
||||
public ExtraGraphicsExtension ModExtension
|
||||
{
|
||||
get
|
||||
{
|
||||
if (modExtension == null)
|
||||
{
|
||||
modExtension = def.GetModExtension<ExtraGraphicsExtension>();
|
||||
if (modExtension == null)
|
||||
{
|
||||
WulaLog.Debug($"Building_ExtraGraphics: No ExtraGraphicsExtension found for {def.defName}");
|
||||
// 创建默认配置避免空引用
|
||||
modExtension = new ExtraGraphicsExtension();
|
||||
}
|
||||
}
|
||||
return modExtension;
|
||||
}
|
||||
}
|
||||
|
||||
// 重写 Graphic 属性返回 null,完全自定义渲染
|
||||
public override Graphic Graphic => null;
|
||||
|
||||
// 获取缓存的图形 - 修改后支持自定义Shader
|
||||
private Graphic GetCachedGraphic(string texturePath, Vector2 scale, Color color, Shader shader)
|
||||
{
|
||||
string cacheKey = $"{texturePath}_{scale.x}_{scale.y}_{color}_{shader?.name ?? "null"}";
|
||||
|
||||
if (!graphicsCache.TryGetValue(cacheKey, out Graphic graphic))
|
||||
{
|
||||
graphic = GraphicDatabase.Get<Graphic_Single>(
|
||||
texturePath,
|
||||
shader ?? ShaderDatabase.TransparentPostLight, // 使用传入的Shader,如果为null则使用默认
|
||||
scale,
|
||||
color);
|
||||
graphicsCache[cacheKey] = graphic;
|
||||
}
|
||||
|
||||
return graphic;
|
||||
}
|
||||
|
||||
// 根据Shader名称获取Shader - 修正版本
|
||||
private Shader GetShaderByName(string shaderName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(shaderName))
|
||||
return ShaderDatabase.TransparentPostLight;
|
||||
|
||||
// 使用switch语句匹配实际可用的Shader
|
||||
switch (shaderName.ToLower())
|
||||
{
|
||||
case "transparent":
|
||||
return ShaderDatabase.Transparent;
|
||||
case "transparentpostlight":
|
||||
return ShaderDatabase.TransparentPostLight;
|
||||
case "transparentplant":
|
||||
return ShaderDatabase.TransparentPlant;
|
||||
case "cutout":
|
||||
return ShaderDatabase.Cutout;
|
||||
case "cutoutcomplex":
|
||||
return ShaderDatabase.CutoutComplex;
|
||||
case "cutoutflying":
|
||||
return ShaderDatabase.CutoutFlying;
|
||||
case "cutoutflying01":
|
||||
return ShaderDatabase.CutoutFlying01;
|
||||
case "terrainfade":
|
||||
return ShaderDatabase.TerrainFade;
|
||||
case "terrainfaderough":
|
||||
return ShaderDatabase.TerrainFadeRough;
|
||||
case "mote":
|
||||
return ShaderDatabase.Mote;
|
||||
case "moteglow":
|
||||
return ShaderDatabase.MoteGlow;
|
||||
case "motepulse":
|
||||
return ShaderDatabase.MotePulse;
|
||||
case "moteglowpulse":
|
||||
return ShaderDatabase.MoteGlowPulse;
|
||||
case "motewater":
|
||||
return ShaderDatabase.MoteWater;
|
||||
case "moteglowdistorted":
|
||||
return ShaderDatabase.MoteGlowDistorted;
|
||||
case "solidcolor":
|
||||
return ShaderDatabase.SolidColor;
|
||||
case "vertexcolor":
|
||||
return ShaderDatabase.VertexColor;
|
||||
case "invisible":
|
||||
return ShaderDatabase.Invisible;
|
||||
case "silhouette":
|
||||
return ShaderDatabase.Silhouette;
|
||||
case "worldterrain":
|
||||
return ShaderDatabase.WorldTerrain;
|
||||
case "worldocean":
|
||||
return ShaderDatabase.WorldOcean;
|
||||
case "metaoverlay":
|
||||
return ShaderDatabase.MetaOverlay;
|
||||
default:
|
||||
WulaLog.Debug($"Building_ExtraGraphics: Shader '{shaderName}' not found, using TransparentPostLight as fallback");
|
||||
return ShaderDatabase.TransparentPostLight;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DrawAt(Vector3 drawLoc, bool flip = false)
|
||||
{
|
||||
// 不调用基类的 DrawAt,完全自定义渲染
|
||||
|
||||
// 更新动画状态
|
||||
UpdateAnimations();
|
||||
// 绘制所有配置的图形层
|
||||
DrawGraphicLayers(drawLoc, flip);
|
||||
// 新增:绘制护盾
|
||||
var shieldComp = this.GetComp<ThingComp_AreaShield>();
|
||||
if (shieldComp != null)
|
||||
{
|
||||
shieldComp.PostDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制所有图形层
|
||||
private void DrawGraphicLayers(Vector3 baseDrawPos, bool flip)
|
||||
{
|
||||
if (ModExtension.graphicLayers == null || ModExtension.graphicLayers.Count == 0)
|
||||
{
|
||||
WulaLog.Debug($"Building_ExtraGraphics: No graphic layers configured for {def.defName}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 按层级排序,确保正确的绘制顺序
|
||||
var sortedLayers = ModExtension.graphicLayers.OrderBy(layer => layer.drawOrder).ToList();
|
||||
|
||||
foreach (var layer in sortedLayers)
|
||||
{
|
||||
DrawGraphicLayer(baseDrawPos, flip, layer);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制单个图形层 - 现在支持三种旋转动画
|
||||
private void DrawGraphicLayer(Vector3 baseDrawPos, bool flip, GraphicLayerData layer)
|
||||
{
|
||||
if (string.IsNullOrEmpty(layer.texturePath))
|
||||
{
|
||||
WulaLog.Debug($"Building_ExtraGraphics: Empty texture path in layer for {def.defName}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取Shader
|
||||
Shader shader = GetShaderByName(layer.shaderName);
|
||||
|
||||
// 获取图形(现在传入Shader)
|
||||
Graphic graphic = GetCachedGraphic(layer.texturePath, layer.scale, layer.color, shader);
|
||||
|
||||
// 计算图层动画偏移
|
||||
Vector3 animationOffset = Vector3.zero;
|
||||
float rotationAngle = 0f;
|
||||
int layerIndex = ModExtension.graphicLayers.IndexOf(layer);
|
||||
|
||||
// 根据动画类型应用不同的动画效果
|
||||
switch (layer.animationType)
|
||||
{
|
||||
case AnimationType.Hover:
|
||||
if (layer.enableAnimation && layerHoverOffsets.ContainsKey(layerIndex))
|
||||
{
|
||||
animationOffset.z = layerHoverOffsets[layerIndex];
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationType.RotateZ:
|
||||
case AnimationType.RotateY:
|
||||
case AnimationType.RotateX:
|
||||
if (layer.enableAnimation && layerRotationAngles.ContainsKey(layerIndex))
|
||||
{
|
||||
rotationAngle = layerRotationAngles[layerIndex];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 最终绘制位置 = 基础位置 + 图层偏移 + 动画偏移
|
||||
Vector3 drawPos = baseDrawPos + layer.offset + animationOffset;
|
||||
|
||||
// 如果启用了旋转动画,使用特殊方法绘制
|
||||
if ((layer.animationType == AnimationType.RotateZ ||
|
||||
layer.animationType == AnimationType.RotateY ||
|
||||
layer.animationType == AnimationType.RotateX) &&
|
||||
layer.enableAnimation)
|
||||
{
|
||||
// 使用自定义旋转绘制方法
|
||||
DrawWithRotation(graphic, drawPos, flip, rotationAngle, layer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 普通绘制
|
||||
graphic.Draw(drawPos, flip ? base.Rotation.Opposite : base.Rotation, this, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用矩阵变换绘制旋转图形 - 支持三种旋转轴
|
||||
private void DrawWithRotation(Graphic graphic, Vector3 drawPos, bool flip, float rotationAngle, GraphicLayerData layer)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取网格和材质
|
||||
Mesh mesh = graphic.MeshAt(flip ? base.Rotation.Opposite : base.Rotation);
|
||||
Material mat = graphic.MatAt(flip ? base.Rotation.Opposite : base.Rotation);
|
||||
|
||||
if (mesh == null || mat == null)
|
||||
{
|
||||
WulaLog.Debug($"Building_ExtraGraphics: Unable to get mesh or material for rotating layer");
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据旋转类型创建不同的旋转矩阵
|
||||
Quaternion rotation = Quaternion.identity;
|
||||
|
||||
switch (layer.animationType)
|
||||
{
|
||||
case AnimationType.RotateZ:
|
||||
// 绕Z轴旋转(2D平面旋转)
|
||||
rotation = Quaternion.Euler(0f, 0f, rotationAngle);
|
||||
break;
|
||||
|
||||
case AnimationType.RotateY:
|
||||
// 绕Y轴旋转(3D旋转,类似旋转门)
|
||||
rotation = Quaternion.Euler(0f, rotationAngle, 0f);
|
||||
break;
|
||||
|
||||
case AnimationType.RotateX:
|
||||
// 绕X轴旋转(3D旋转,类似翻跟斗)
|
||||
rotation = Quaternion.Euler(rotationAngle, 0f, 0f);
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果图层有旋转中心偏移,需要调整位置
|
||||
Vector3 pivotOffset = new Vector3(layer.pivotOffset.x, 0, layer.pivotOffset.y);
|
||||
|
||||
// 最终绘制位置 = 基础位置 + 图层偏移 + 旋转中心偏移
|
||||
Vector3 finalDrawPos = drawPos + pivotOffset;
|
||||
|
||||
// 创建变换矩阵
|
||||
// 注意:Graphic已经应用了缩放,所以这里使用Vector3.one
|
||||
Matrix4x4 matrix = Matrix4x4.TRS(
|
||||
finalDrawPos, // 位置
|
||||
rotation, // 旋转
|
||||
Vector3.one // 缩放(已由Graphic处理)
|
||||
);
|
||||
|
||||
// 使用RimWorld的绘制方法
|
||||
GenDraw.DrawMeshNowOrLater(mesh, matrix, mat, false);
|
||||
|
||||
// 如果需要双面渲染,再绘制一次
|
||||
if (layer.doubleSided)
|
||||
{
|
||||
GenDraw.DrawMeshNowOrLater(mesh, matrix, mat, false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WulaLog.Debug($"Building_ExtraGraphics: Error drawing rotating layer: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// 更新所有图层的动画状态
|
||||
private void UpdateAnimations()
|
||||
{
|
||||
int currentTick = Find.TickManager.TicksGame;
|
||||
|
||||
if (currentTick != lastTick)
|
||||
{
|
||||
// 更新每个图层的动画
|
||||
for (int i = 0; i < ModExtension.graphicLayers.Count; i++)
|
||||
{
|
||||
var layer = ModExtension.graphicLayers[i];
|
||||
|
||||
if (!layer.enableAnimation)
|
||||
continue;
|
||||
|
||||
// 初始化动画时间
|
||||
if (!layerAnimationTimes.ContainsKey(i))
|
||||
{
|
||||
layerAnimationTimes[i] = layer.animationStartTime;
|
||||
// 为旋转动画初始化旋转角度
|
||||
if (layer.animationType == AnimationType.RotateZ ||
|
||||
layer.animationType == AnimationType.RotateY ||
|
||||
layer.animationType == AnimationType.RotateX)
|
||||
{
|
||||
layerRotationAngles[i] = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新动画时间
|
||||
layerAnimationTimes[i] += Time.deltaTime;
|
||||
|
||||
// 根据动画类型更新不同的状态
|
||||
switch (layer.animationType)
|
||||
{
|
||||
case AnimationType.Hover:
|
||||
// 计算该图层的悬浮偏移
|
||||
float hoverSpeed = layer.animationSpeed > 0 ? layer.animationSpeed : ModExtension.globalAnimationSpeed;
|
||||
float hoverIntensity = layer.animationIntensity > 0 ? layer.animationIntensity : ModExtension.globalAnimationIntensity;
|
||||
|
||||
float hoverOffset = Mathf.Sin(layerAnimationTimes[i] * hoverSpeed + layer.animationPhase) * hoverIntensity;
|
||||
layerHoverOffsets[i] = hoverOffset;
|
||||
break;
|
||||
|
||||
case AnimationType.RotateZ:
|
||||
case AnimationType.RotateY:
|
||||
case AnimationType.RotateX:
|
||||
// 计算该图层的旋转角度
|
||||
float rotateSpeed = layer.animationSpeed > 0 ? layer.animationSpeed : ModExtension.globalAnimationSpeed;
|
||||
|
||||
// 旋转角度计算:动画时间 × 旋转速度(度/秒)
|
||||
float rotationAngle = layerAnimationTimes[i] * rotateSpeed;
|
||||
|
||||
// 如果设置了动画强度且小于360,则限制旋转范围
|
||||
if (layer.animationIntensity > 0 && layer.animationIntensity < 360f)
|
||||
{
|
||||
// 使用正弦波创建来回旋转效果
|
||||
rotationAngle = Mathf.Sin(layerAnimationTimes[i] * rotateSpeed * Mathf.Deg2Rad) * layer.animationIntensity;
|
||||
}
|
||||
else if (layer.animationIntensity >= 360f)
|
||||
{
|
||||
// 完整旋转:取模确保在0-360度之间
|
||||
rotationAngle %= 360f;
|
||||
}
|
||||
|
||||
layerRotationAngles[i] = rotationAngle;
|
||||
|
||||
// 调试输出
|
||||
if (DebugSettings.godMode && i == 0 && Find.TickManager.TicksGame % 60 == 0)
|
||||
{
|
||||
// 只在开发模式下,每60帧输出一次第一条图层的旋转信息
|
||||
WulaLog.Debug($"{layer.animationType} 图层 {i}: 角度={rotationAngle:F1}°, 时间={layerAnimationTimes[i]:F2}s, 速度={rotateSpeed}°/s");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lastTick = currentTick;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存和加载
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
// 保存自定义状态(如果需要)
|
||||
}
|
||||
|
||||
// 调试方法:手动触发旋转测试
|
||||
public void TestRotation()
|
||||
{
|
||||
for (int i = 0; i < ModExtension.graphicLayers.Count; i++)
|
||||
{
|
||||
var layer = ModExtension.graphicLayers[i];
|
||||
if (layer.animationType == AnimationType.RotateZ ||
|
||||
layer.animationType == AnimationType.RotateY ||
|
||||
layer.animationType == AnimationType.RotateX)
|
||||
{
|
||||
layerRotationAngles[i] = 45f; // 设置为45度测试
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画类型枚举 - 现在有五种动画类型
|
||||
public enum AnimationType
|
||||
{
|
||||
None, // 无动画
|
||||
Hover, // 上下浮动
|
||||
RotateZ, // 绕Z轴旋转(2D平面旋转)- 原来的Rotate
|
||||
RotateY, // 绕Y轴旋转(3D旋转,类似旋转门)- 新增
|
||||
RotateX // 绕X轴旋转(3D旋转,类似翻跟斗)- 新增
|
||||
}
|
||||
|
||||
// 主要的 ModExtension 定义
|
||||
public class ExtraGraphicsExtension : DefModExtension
|
||||
{
|
||||
// 全局动画参数(作为默认值)
|
||||
public float globalAnimationSpeed = 2f; // 全局动画速度
|
||||
public float globalAnimationIntensity = 0.1f; // 全局动画强度
|
||||
|
||||
// 图形层配置
|
||||
public List<GraphicLayerData> graphicLayers = new List<GraphicLayerData>();
|
||||
|
||||
public ExtraGraphicsExtension()
|
||||
{
|
||||
// 默认配置,避免空列表
|
||||
if (graphicLayers == null)
|
||||
{
|
||||
graphicLayers = new List<GraphicLayerData>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单个图形层的配置数据 - 增强版
|
||||
public class GraphicLayerData
|
||||
{
|
||||
// 基础配置
|
||||
public string texturePath; // 纹理路径(必需)
|
||||
public Vector2 scale = Vector2.one; // 缩放比例
|
||||
public Color color = Color.white; // 颜色
|
||||
public int drawOrder = 0; // 绘制顺序(数字小的先绘制)
|
||||
public string shaderName = "TransparentPostLight"; // Shader名称
|
||||
|
||||
// 位置配置 - 使用环世界坐标系
|
||||
// X: 左右偏移, Y: 图层深度, Z: 上下偏移
|
||||
public Vector3 offset = Vector3.zero;
|
||||
|
||||
// 动画配置
|
||||
public AnimationType animationType = AnimationType.Hover; // 动画类型
|
||||
public bool enableAnimation = true; // 是否启用动画
|
||||
public float animationSpeed = 0f; // 动画速度(0表示使用全局速度)
|
||||
public float animationIntensity = 0f; // 动画强度(0表示使用全局强度)
|
||||
public float animationPhase = 0f; // 动画相位(用于错开动画)
|
||||
public float animationStartTime = 0f; // 动画开始时间偏移
|
||||
|
||||
// 旋转动画专用配置
|
||||
public Vector2 pivotOffset = Vector2.zero; // 旋转中心偏移(相对于图层的中心)
|
||||
public bool doubleSided = false; // 是否双面渲染(对于旋转物体)
|
||||
|
||||
// 兼容旧字段(为了向后兼容)
|
||||
[Obsolete("Use enableAnimation instead")]
|
||||
public bool enableHover
|
||||
{
|
||||
get => enableAnimation && animationType == AnimationType.Hover;
|
||||
set
|
||||
{
|
||||
enableAnimation = value;
|
||||
if (value && animationType == AnimationType.None)
|
||||
animationType = AnimationType.Hover;
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use animationSpeed instead")]
|
||||
public float hoverSpeed
|
||||
{
|
||||
get => animationSpeed;
|
||||
set => animationSpeed = value;
|
||||
}
|
||||
|
||||
[Obsolete("Use animationIntensity instead")]
|
||||
public float hoverIntensity
|
||||
{
|
||||
get => animationIntensity;
|
||||
set => animationIntensity = value;
|
||||
}
|
||||
|
||||
[Obsolete("Use animationPhase instead")]
|
||||
public float hoverPhase
|
||||
{
|
||||
get => animationPhase;
|
||||
set => animationPhase = value;
|
||||
}
|
||||
|
||||
// 向后兼容:Rotate应该映射到RotateZ
|
||||
[Obsolete("Use animationType instead")]
|
||||
public AnimationType animationTypeCompat
|
||||
{
|
||||
get => animationType;
|
||||
set
|
||||
{
|
||||
animationType = value;
|
||||
// 如果旧值是Rotate,映射到RotateZ
|
||||
if (animationType == AnimationType.RotateZ)
|
||||
animationType = AnimationType.RotateZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
230
Source/WulaFallenEmpire/Building/Building_MapObserver.cs
Normal file
230
Source/WulaFallenEmpire/Building/Building_MapObserver.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using RimWorld.Planet;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class Building_MapObserver : Building
|
||||
{
|
||||
public MapParent observedMap;
|
||||
|
||||
private CompPowerTrader compPower;
|
||||
|
||||
// 静态列表跟踪所有活跃的观察者
|
||||
public static HashSet<Building_MapObserver> activeObservers = new HashSet<Building_MapObserver>();
|
||||
|
||||
public override void SpawnSetup(Map map, bool respawningAfterLoad)
|
||||
{
|
||||
base.SpawnSetup(map, respawningAfterLoad);
|
||||
compPower = this.TryGetComp<CompPowerTrader>();
|
||||
|
||||
// 如果正在观察地图且建筑正常,注册到活跃列表
|
||||
if (observedMap != null && (compPower == null || compPower.PowerOn))
|
||||
{
|
||||
activeObservers.Add(this);
|
||||
}
|
||||
}
|
||||
|
||||
public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish)
|
||||
{
|
||||
// 建筑被销毁时停止监测
|
||||
DisposeObservedMapIfEmpty();
|
||||
activeObservers.Remove(this);
|
||||
base.DeSpawn(mode);
|
||||
}
|
||||
|
||||
public override IEnumerable<Gizmo> GetGizmos()
|
||||
{
|
||||
foreach (Gizmo gizmo in base.GetGizmos())
|
||||
{
|
||||
yield return gizmo;
|
||||
}
|
||||
|
||||
// 只有在有电力且属于玩家时才显示控制按钮
|
||||
if (Faction == Faction.OfPlayer && (compPower == null || compPower.PowerOn))
|
||||
{
|
||||
// 开始监测按钮
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "开始监测地图",
|
||||
defaultDesc = "选择一个世界位置进行监测",
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/ShowMap"),
|
||||
action = delegate
|
||||
{
|
||||
CameraJumper.TryShowWorld();
|
||||
Find.WorldTargeter.BeginTargeting(ChooseWorldTarget, canTargetTiles: true);
|
||||
}
|
||||
};
|
||||
|
||||
// 如果正在监测地图,显示停止按钮
|
||||
if (observedMap != null)
|
||||
{
|
||||
if (observedMap.Destroyed)
|
||||
{
|
||||
observedMap = null;
|
||||
activeObservers.Remove(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "停止监测",
|
||||
defaultDesc = $"停止监测 {observedMap.Label}",
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/DesirePower"),
|
||||
action = delegate
|
||||
{
|
||||
StopObserving();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ChooseWorldTarget(GlobalTargetInfo target)
|
||||
{
|
||||
DisposeObservedMapIfEmpty();
|
||||
|
||||
if (target.WorldObject != null && target.WorldObject is MapParent mapParent)
|
||||
{
|
||||
// 开始监测选中的地图
|
||||
observedMap = mapParent;
|
||||
activeObservers.Add(this);
|
||||
|
||||
// 确保地图被生成并取消迷雾
|
||||
LongEventHandler.QueueLongEvent(delegate
|
||||
{
|
||||
Map map = GetOrGenerateMapUtility.GetOrGenerateMap(target.Tile, null);
|
||||
if (map != null)
|
||||
{
|
||||
// 取消迷雾获得完整视野
|
||||
map.fogGrid.ClearAllFog();
|
||||
|
||||
// 记录日志以便调试
|
||||
WulaLog.Debug($"[MapObserver] 开始监测地图: {mapParent.Label} at tile {target.Tile}");
|
||||
}
|
||||
}, "GeneratingMap", doAsynchronously: false, null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 在空地创建新监测点
|
||||
if (target.WorldObject == null && !Find.World.Impassable(target.Tile))
|
||||
{
|
||||
LongEventHandler.QueueLongEvent(delegate
|
||||
{
|
||||
// 创建新的玩家定居点用于监测
|
||||
SettleUtility.AddNewHome(target.Tile, Faction.OfPlayer);
|
||||
Map map = GetOrGenerateMapUtility.GetOrGenerateMap(target.Tile, Find.World.info.initialMapSize, null);
|
||||
observedMap = map.Parent;
|
||||
activeObservers.Add(this);
|
||||
|
||||
// 取消迷雾获得完整视野
|
||||
map.fogGrid.ClearAllFog();
|
||||
|
||||
// 设置监测点名称
|
||||
if (observedMap is Settlement settlement)
|
||||
{
|
||||
settlement.Name = $"监测点-{thingIDNumber}";
|
||||
WulaLog.Debug($"[MapObserver] 创建新监测点: {settlement.Name} at tile {target.Tile}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果observedMap不是Settlement,使用Label属性
|
||||
WulaLog.Debug($"[MapObserver] 创建新监测点: {observedMap.Label} at tile {target.Tile}");
|
||||
}
|
||||
|
||||
}, "GeneratingMap", doAsynchronously: false, null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Messages.Message("无法监测该位置", MessageTypeDefOf.RejectInput);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void StopObserving()
|
||||
{
|
||||
DisposeObservedMapIfEmpty();
|
||||
observedMap = null;
|
||||
activeObservers.Remove(this);
|
||||
}
|
||||
|
||||
private void DisposeObservedMapIfEmpty()
|
||||
{
|
||||
if (observedMap != null && observedMap.Map != null &&
|
||||
!observedMap.Map.mapPawns.AnyColonistSpawned &&
|
||||
!observedMap.Map.listerBuildings.allBuildingsColonist.Any() &&
|
||||
observedMap.Faction == Faction.OfPlayer)
|
||||
{
|
||||
// 只有在没有殖民者、没有玩家建筑的情况下才销毁
|
||||
Current.Game.DeinitAndRemoveMap(observedMap.Map, notifyPlayer: false);
|
||||
if (!observedMap.Destroyed)
|
||||
{
|
||||
Find.World.worldObjects.Remove(observedMap);
|
||||
}
|
||||
WulaLog.Debug($"[MapObserver] 清理空置监测地图: {observedMap.Label}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ReceiveCompSignal(string signal)
|
||||
{
|
||||
base.ReceiveCompSignal(signal);
|
||||
|
||||
// 断电或被关闭时停止监测
|
||||
if (observedMap != null && (signal == "PowerTurnedOff" || signal == "FlickedOff"))
|
||||
{
|
||||
WulaLog.Debug($"[MapObserver] 电力中断,停止监测: {observedMap.Label}");
|
||||
StopObserving();
|
||||
}
|
||||
// 恢复电力时重新注册
|
||||
else if (observedMap != null && (signal == "PowerTurnedOn" || signal == "FlickedOn"))
|
||||
{
|
||||
activeObservers.Add(this);
|
||||
WulaLog.Debug($"[MapObserver] 电力恢复,继续监测: {observedMap.Label}");
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetInspectString()
|
||||
{
|
||||
string text = base.GetInspectString();
|
||||
|
||||
if (observedMap != null)
|
||||
{
|
||||
if (!text.NullOrEmpty())
|
||||
{
|
||||
text += "\n";
|
||||
}
|
||||
text += $"正在监测: {observedMap.Label}";
|
||||
|
||||
// 显示电力状态
|
||||
if (compPower != null && !compPower.PowerOn)
|
||||
{
|
||||
text += " (电力中断)";
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_References.Look(ref observedMap, "observedMap");
|
||||
|
||||
// 加载后重新注册到活跃列表
|
||||
if (Scribe.mode == LoadSaveMode.PostLoadInit && observedMap != null && (compPower == null || compPower.PowerOn))
|
||||
{
|
||||
activeObservers.Add(this);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查这个观察者是否正在监测指定的地图
|
||||
public bool IsObservingMap(MapParent mapParent)
|
||||
{
|
||||
return observedMap == mapParent && (compPower == null || compPower.PowerOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
924
Source/WulaFallenEmpire/Building/Building_TurretGunHasSpeed.cs
Normal file
924
Source/WulaFallenEmpire/Building/Building_TurretGunHasSpeed.cs
Normal file
@@ -0,0 +1,924 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
using Verse.AI.Group;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class ModExt_HasSpeedTurret : DefModExtension
|
||||
{
|
||||
public float speed = 1f;
|
||||
}
|
||||
/// <summary>
|
||||
/// 非瞬时瞄准的炮塔建筑类
|
||||
/// 继承自原版炮塔,增加了平滑旋转瞄准功能
|
||||
/// </summary>
|
||||
public class Building_TurretGunHasSpeed : Building_TurretGun
|
||||
{
|
||||
// 当前炮塔角度
|
||||
public float curAngle;
|
||||
|
||||
/// <summary>
|
||||
/// 旋转速度属性
|
||||
/// 从Mod扩展配置中获取旋转速度,如果没有配置则使用默认值1f
|
||||
/// </summary>
|
||||
public float rotateSpeed
|
||||
{
|
||||
get
|
||||
{
|
||||
ModExt_HasSpeedTurret ext = this.ext;
|
||||
return ext.speed;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Mod扩展配置属性
|
||||
/// 获取炮塔定义的Mod扩展配置
|
||||
/// </summary>
|
||||
public ModExt_HasSpeedTurret ext
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.def.GetModExtension<ModExt_HasSpeedTurret>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 炮塔方向向量
|
||||
/// 根据当前角度计算炮塔的朝向向量
|
||||
/// </summary>
|
||||
public Vector3 turretOrientation
|
||||
{
|
||||
get
|
||||
{
|
||||
return Vector3.forward.RotatedBy(this.curAngle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标角度差
|
||||
/// 计算当前炮塔方向与目标方向之间的角度差
|
||||
/// </summary>
|
||||
public float deltaAngle
|
||||
{
|
||||
get
|
||||
{
|
||||
return (this.currentTargetInt == null) ? 0f : Vector3.SignedAngle(this.turretOrientation, (this.currentTargetInt.CenterVector3 - this.DrawPos).Yto0(), Vector3.up);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据保存和加载
|
||||
/// 重写ExposeData以保存和加载当前角度数据
|
||||
/// </summary>
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_Values.Look<float>(ref this.curAngle, "curAngle", 0f, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以攻击目标(LocalTargetInfo重载)
|
||||
/// </summary>
|
||||
/// <param name="t">目标信息</param>
|
||||
/// <returns>是否可以攻击</returns>
|
||||
private bool CanAttackTarget(LocalTargetInfo t)
|
||||
{
|
||||
return this.CanAttackTarget(t.CenterVector3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以攻击目标(Thing重载)
|
||||
/// </summary>
|
||||
/// <param name="t">目标物体</param>
|
||||
/// <returns>是否可以攻击</returns>
|
||||
private bool CanAttackTarget(Thing t)
|
||||
{
|
||||
return this.CanAttackTarget(t.DrawPos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以攻击目标(Vector3重载)
|
||||
/// 判断目标是否在当前炮塔的瞄准范围内
|
||||
/// </summary>
|
||||
/// <param name="t">目标位置</param>
|
||||
/// <returns>是否可以攻击</returns>
|
||||
private bool CanAttackTarget(Vector3 t)
|
||||
{
|
||||
return Vector3.Angle(this.turretOrientation, (t - this.DrawPos).Yto0()) <= this.rotateSpeed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每帧更新
|
||||
/// 处理炮塔的旋转逻辑
|
||||
/// </summary>
|
||||
protected override void Tick()
|
||||
{
|
||||
// 如果炮塔处于激活状态且有目标
|
||||
if (base.Active && this.currentTargetInt != null)
|
||||
{
|
||||
// 如果准备开火但角度差过大,延迟开火
|
||||
if (this.burstWarmupTicksLeft == 1 && Mathf.Abs(this.deltaAngle) > this.rotateSpeed)
|
||||
{
|
||||
this.burstWarmupTicksLeft++;
|
||||
}
|
||||
|
||||
// 根据角度差更新当前角度
|
||||
this.curAngle += ((Mathf.Abs(this.deltaAngle) - this.rotateSpeed > 0f) ?
|
||||
(Mathf.Sign(this.deltaAngle) * this.rotateSpeed) : this.deltaAngle);
|
||||
}
|
||||
|
||||
base.Tick();
|
||||
// 规范化角度值到0-360度范围
|
||||
this.curAngle = this.Trim(this.curAngle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角度规范化
|
||||
/// 将角度值限制在0-360度范围内
|
||||
/// </summary>
|
||||
/// <param name="angle">输入角度</param>
|
||||
/// <returns>规范化后的角度</returns>
|
||||
protected float Trim(float angle)
|
||||
{
|
||||
if (angle > 360f)
|
||||
{
|
||||
angle -= 360f;
|
||||
}
|
||||
if (angle < 0f)
|
||||
{
|
||||
angle += 360f;
|
||||
}
|
||||
return angle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制炮塔
|
||||
/// 设置炮塔顶部的旋转角度
|
||||
/// </summary>
|
||||
/// <param name="drawLoc">绘制位置</param>
|
||||
/// <param name="flip">是否翻转</param>
|
||||
protected override void DrawAt(Vector3 drawLoc, bool flip = false)
|
||||
{
|
||||
this.top.CurRotation = this.curAngle;
|
||||
base.DrawAt(drawLoc, flip);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标搜索器
|
||||
/// 如果有人操作则返回操作者,否则返回炮塔自身
|
||||
/// </summary>
|
||||
/// <returns>目标搜索器</returns>
|
||||
private IAttackTargetSearcher TargSearcher()
|
||||
{
|
||||
if (this.mannableComp != null && this.mannableComp.MannedNow)
|
||||
{
|
||||
return this.mannableComp.ManningPawn;
|
||||
}
|
||||
else
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查目标是否有效
|
||||
/// 过滤不适合攻击的目标
|
||||
/// </summary>
|
||||
/// <param name="t">目标物体</param>
|
||||
/// <returns>目标是否有效</returns>
|
||||
private bool IsValidTarget(Thing t)
|
||||
{
|
||||
Pawn pawn = t as Pawn;
|
||||
if (pawn != null)
|
||||
{
|
||||
// 玩家派系的炮塔不攻击囚犯
|
||||
if (base.Faction == Faction.OfPlayer && pawn.IsPrisoner)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查弹道是否会被厚屋顶阻挡
|
||||
if (this.AttackVerb.ProjectileFliesOverhead())
|
||||
{
|
||||
RoofDef roofDef = base.Map.roofGrid.RoofAt(t.Position);
|
||||
if (roofDef != null && roofDef.isThickRoof)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 无人操作的机械炮塔不攻击友好机械单位
|
||||
if (this.mannableComp == null)
|
||||
{
|
||||
return !GenAI.MachinesLike(base.Faction, pawn);
|
||||
}
|
||||
|
||||
// 有人操作的炮塔不攻击玩家动物
|
||||
if (pawn.RaceProps.Animal && pawn.Faction == Faction.OfPlayer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试寻找新目标
|
||||
/// 重写目标选择逻辑,支持角度限制
|
||||
/// </summary>
|
||||
/// <returns>新的目标信息</returns>
|
||||
public override LocalTargetInfo TryFindNewTarget()
|
||||
{
|
||||
IAttackTargetSearcher attackTargetSearcher = this.TargSearcher();
|
||||
Faction faction = attackTargetSearcher.Thing.Faction;
|
||||
float range = this.AttackVerb.verbProps.range;
|
||||
|
||||
Building t;
|
||||
// 50%概率优先攻击殖民者建筑(如果敌对且使用抛射武器)
|
||||
if (Rand.Value < 0.5f && this.AttackVerb.ProjectileFliesOverhead() &&
|
||||
faction.HostileTo(Faction.OfPlayer) &&
|
||||
base.Map.listerBuildings.allBuildingsColonist.Where(delegate (Building x)
|
||||
{
|
||||
float minRange = this.AttackVerb.verbProps.EffectiveMinRange(x, this);
|
||||
float distanceSquared = (float)x.Position.DistanceToSquared(this.Position);
|
||||
return distanceSquared > minRange * minRange && distanceSquared < range * range;
|
||||
}).TryRandomElement(out t))
|
||||
{
|
||||
return t;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 设置目标扫描标志
|
||||
TargetScanFlags targetScanFlags = TargetScanFlags.NeedThreat | TargetScanFlags.NeedAutoTargetable;
|
||||
|
||||
if (!this.AttackVerb.ProjectileFliesOverhead())
|
||||
{
|
||||
targetScanFlags |= TargetScanFlags.NeedLOSToAll;
|
||||
targetScanFlags |= TargetScanFlags.LOSBlockableByGas;
|
||||
}
|
||||
|
||||
if (this.AttackVerb.IsIncendiary_Ranged())
|
||||
{
|
||||
targetScanFlags |= TargetScanFlags.NeedNonBurning;
|
||||
}
|
||||
|
||||
if (this.def.building.IsMortar)
|
||||
{
|
||||
targetScanFlags |= TargetScanFlags.NeedNotUnderThickRoof;
|
||||
}
|
||||
|
||||
// 使用角度感知的目标查找器
|
||||
return (Thing)AttackTargetFinderAngle.BestShootTargetFromCurrentPosition(
|
||||
attackTargetSearcher, targetScanFlags, this.turretOrientation,
|
||||
new Predicate<Thing>(this.IsValidTarget), 0f, 9999f);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 攻击目标查找器(角度优化版)
|
||||
/// 提供基于角度优化的攻击目标选择功能
|
||||
/// </summary>
|
||||
public static class AttackTargetFinderAngle
|
||||
{
|
||||
// 友军误伤评分偏移量常量
|
||||
private const float FriendlyFireScoreOffsetPerHumanlikeOrMechanoid = 18f; // 每人类或机械族的友军误伤分数偏移
|
||||
private const float FriendlyFireScoreOffsetPerAnimal = 7f; // 每动物的友军误伤分数偏移
|
||||
private const float FriendlyFireScoreOffsetPerNonPawn = 10f; // 每非pawn单位的友军误伤分数偏移
|
||||
private const float FriendlyFireScoreOffsetSelf = 40f; // 对自己造成误伤的分数偏移
|
||||
// 临时目标列表,用于缓存计算过程中的目标
|
||||
private static List<IAttackTarget> tmpTargets = new List<IAttackTarget>(128);
|
||||
|
||||
// 可用射击目标及其分数的列表
|
||||
private static List<Pair<IAttackTarget, float>> availableShootingTargets = new List<Pair<IAttackTarget, float>>();
|
||||
|
||||
// 临时存储目标分数的列表
|
||||
private static List<float> tmpTargetScores = new List<float>();
|
||||
|
||||
// 临时存储是否可以向目标射击的列表
|
||||
private static List<bool> tmpCanShootAtTarget = new List<bool>();
|
||||
/// <summary>
|
||||
/// 从当前位置寻找最佳射击目标
|
||||
/// </summary>
|
||||
/// <param name="searcher">搜索者(攻击目标搜索器)</param>
|
||||
/// <param name="flags">目标扫描标志</param>
|
||||
/// <param name="angle">射击角度</param>
|
||||
/// <param name="validator">目标验证器(可选)</param>
|
||||
/// <param name="minDistance">最小距离(默认0)</param>
|
||||
/// <param name="maxDistance">最大距离(默认9999)</param>
|
||||
/// <returns>最佳攻击目标,如果没有则返回null</returns>
|
||||
public static IAttackTarget BestShootTargetFromCurrentPosition(
|
||||
IAttackTargetSearcher searcher,
|
||||
TargetScanFlags flags,
|
||||
Vector3 angle,
|
||||
Predicate<Thing> validator = null,
|
||||
float minDistance = 0f,
|
||||
float maxDistance = 9999f)
|
||||
{
|
||||
// 获取当前有效动词(武器)
|
||||
Verb currentEffectiveVerb = searcher.CurrentEffectiveVerb;
|
||||
|
||||
// 检查是否有攻击动词
|
||||
if (currentEffectiveVerb == null)
|
||||
{
|
||||
WulaLog.Debug("BestShootTargetFromCurrentPosition with " + searcher.ToStringSafe<IAttackTargetSearcher>() + " who has no attack verb.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算实际的最小和最大距离,考虑武器的属性
|
||||
float actualMinDistance = Mathf.Max(minDistance, currentEffectiveVerb.verbProps.minRange);
|
||||
float actualMaxDistance = Mathf.Min(maxDistance, currentEffectiveVerb.verbProps.range);
|
||||
|
||||
// 调用主要的目标查找方法
|
||||
return BestAttackTarget(
|
||||
searcher,
|
||||
flags,
|
||||
angle,
|
||||
validator,
|
||||
actualMinDistance,
|
||||
actualMaxDistance,
|
||||
default(IntVec3),
|
||||
float.MaxValue,
|
||||
false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找最佳攻击目标(核心方法)
|
||||
/// </summary>
|
||||
/// <param name="searcher">搜索者</param>
|
||||
/// <param name="flags">目标扫描标志</param>
|
||||
/// <param name="angle">射击角度</param>
|
||||
/// <param name="validator">目标验证器</param>
|
||||
/// <param name="minDist">最小距离</param>
|
||||
/// <param name="maxDist">最大距离</param>
|
||||
/// <param name="locus">搜索中心点</param>
|
||||
/// <param name="maxTravelRadiusFromLocus">从中心点的最大移动半径</param>
|
||||
/// <param name="canTakeTargetsCloserThanEffectiveMinRange">是否可以攻击比有效最小距离更近的目标</param>
|
||||
/// <returns>最佳攻击目标</returns>
|
||||
public static IAttackTarget BestAttackTarget(
|
||||
IAttackTargetSearcher searcher,
|
||||
TargetScanFlags flags,
|
||||
Vector3 angle,
|
||||
Predicate<Thing> validator = null,
|
||||
float minDist = 0f,
|
||||
float maxDist = 9999f,
|
||||
IntVec3 locus = default(IntVec3),
|
||||
float maxTravelRadiusFromLocus = float.MaxValue,
|
||||
bool canTakeTargetsCloserThanEffectiveMinRange = true)
|
||||
{
|
||||
// 获取搜索者的Thing对象和当前有效动词
|
||||
Thing searcherThing = searcher.Thing;
|
||||
Verb verb = searcher.CurrentEffectiveVerb;
|
||||
|
||||
// 验证攻击动词是否存在
|
||||
if (verb == null)
|
||||
{
|
||||
WulaLog.Debug("BestAttackTarget with " + searcher.ToStringSafe<IAttackTargetSearcher>() + " who has no attack verb.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 初始化各种标志和参数
|
||||
bool onlyTargetMachines = verb.IsEMP(); // 是否只瞄准机械单位(EMP武器)
|
||||
float minDistSquared = minDist * minDist; // 最小距离的平方(用于距离比较优化)
|
||||
|
||||
// 计算从搜索中心点的最大距离平方
|
||||
float maxLocusDist = maxTravelRadiusFromLocus + verb.verbProps.range;
|
||||
float maxLocusDistSquared = maxLocusDist * maxLocusDist;
|
||||
|
||||
// LOS(视线)验证器,用于检查是否被烟雾阻挡
|
||||
Predicate<IntVec3> losValidator = null;
|
||||
if ((flags & TargetScanFlags.LOSBlockableByGas) > TargetScanFlags.None)
|
||||
{
|
||||
losValidator = (IntVec3 vec3) => !vec3.AnyGas(searcherThing.Map, GasType.BlindSmoke);
|
||||
}
|
||||
|
||||
// 获取潜在目标列表
|
||||
tmpTargets.Clear();
|
||||
tmpTargets.AddRange(searcherThing.Map.attackTargetsCache.GetPotentialTargetsFor(searcher));
|
||||
|
||||
// 移除非战斗人员(根据标志)
|
||||
tmpTargets.RemoveAll(t => ShouldIgnoreNoncombatant(searcherThing, t, flags));
|
||||
|
||||
// 内部验证器函数
|
||||
bool InnerValidator(IAttackTarget target, Predicate<IntVec3> losValidator)
|
||||
{
|
||||
Thing targetThing = target.Thing;
|
||||
if (target == searcher)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (minDistSquared > 0f && (float)(searcherThing.Position - targetThing.Position).LengthHorizontalSquared < minDistSquared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!canTakeTargetsCloserThanEffectiveMinRange)
|
||||
{
|
||||
float num3 = verb.verbProps.EffectiveMinRange(targetThing, searcherThing);
|
||||
if (num3 > 0f && (float)(searcherThing.Position - targetThing.Position).LengthHorizontalSquared < num3 * num3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxTravelRadiusFromLocus < 9999f && (float)(targetThing.Position - locus).LengthHorizontalSquared > maxLocusDistSquared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!searcherThing.HostileTo(targetThing))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validator != null && !validator(targetThing))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if ((flags & TargetScanFlags.NeedNotUnderThickRoof) != 0)
|
||||
{
|
||||
RoofDef roof = targetThing.Position.GetRoof(targetThing.Map);
|
||||
if (roof != null && roof.isThickRoof)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ((flags & TargetScanFlags.NeedLOSToAll) != 0)
|
||||
{
|
||||
if (losValidator != null && (!losValidator(searcherThing.Position) || !losValidator(targetThing.Position)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!searcherThing.CanSee(targetThing))
|
||||
{
|
||||
if (target is Pawn)
|
||||
{
|
||||
if ((flags & TargetScanFlags.NeedLOSToPawns) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if ((flags & TargetScanFlags.NeedLOSToNonPawns) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (((flags & TargetScanFlags.NeedThreat) != 0 || (flags & TargetScanFlags.NeedAutoTargetable) != 0) && target.ThreatDisabled(searcher))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((flags & TargetScanFlags.NeedAutoTargetable) != 0 && !AttackTargetFinder.IsAutoTargetable(target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((flags & TargetScanFlags.NeedActiveThreat) != 0 && !GenHostility.IsActiveThreatTo(target, searcher.Thing.Faction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Pawn pawn = target as Pawn;
|
||||
if (onlyTargetMachines && pawn != null && pawn.RaceProps.IsFlesh)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((flags & TargetScanFlags.NeedNonBurning) != 0 && targetThing.IsBurning())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (searcherThing.def.race != null && (int)searcherThing.def.race.intelligence >= 2)
|
||||
{
|
||||
CompExplosive compExplosive = targetThing.TryGetComp<CompExplosive>();
|
||||
if (compExplosive != null && compExplosive.wickStarted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 距离验证
|
||||
if (!targetThing.Position.InHorDistOf(searcherThing.Position, maxDist))
|
||||
return false;
|
||||
|
||||
// 最小距离验证
|
||||
if (!canTakeTargetsCloserThanEffectiveMinRange &&
|
||||
(float)(searcherThing.Position - targetThing.Position).LengthHorizontalSquared < minDistSquared)
|
||||
return false;
|
||||
|
||||
// 中心点距离验证
|
||||
if (locus.IsValid &&
|
||||
(float)(locus - targetThing.Position).LengthHorizontalSquared > maxLocusDistSquared)
|
||||
return false;
|
||||
|
||||
// 自定义验证器
|
||||
if (validator != null && !validator(targetThing))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否有可以直接射击的目标
|
||||
bool hasDirectShootTarget = false;
|
||||
for (int i = 0; i < tmpTargets.Count; i++)
|
||||
{
|
||||
IAttackTarget attackTarget = tmpTargets[i];
|
||||
if (attackTarget.Thing.Position.InHorDistOf(searcherThing.Position, maxDist) &&
|
||||
InnerValidator(attackTarget, losValidator) &&
|
||||
CanShootAtFromCurrentPosition(attackTarget, searcher, verb))
|
||||
{
|
||||
hasDirectShootTarget = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
IAttackTarget bestTarget;
|
||||
|
||||
if (hasDirectShootTarget)
|
||||
{
|
||||
// 如果有可以直接射击的目标,使用基于分数的随机选择
|
||||
tmpTargets.RemoveAll(x => !x.Thing.Position.InHorDistOf(searcherThing.Position, maxDist) || !InnerValidator(x, losValidator));
|
||||
bestTarget = GetRandomShootingTargetByScore(tmpTargets, searcher, verb, angle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 否则使用最近的目标选择策略
|
||||
bool needReachableIfCantHit = (flags & TargetScanFlags.NeedReachableIfCantHitFromMyPos) > TargetScanFlags.None;
|
||||
bool needReachable = (flags & TargetScanFlags.NeedReachable) > TargetScanFlags.None;
|
||||
|
||||
Predicate<Thing> reachableValidator;
|
||||
if (!needReachableIfCantHit || needReachable)
|
||||
{
|
||||
reachableValidator = (Thing t) => InnerValidator((IAttackTarget)t, losValidator);
|
||||
}
|
||||
else
|
||||
{
|
||||
reachableValidator = (Thing t) => InnerValidator((IAttackTarget)t, losValidator) &&
|
||||
CanShootAtFromCurrentPosition((IAttackTarget)t, searcher, verb);
|
||||
}
|
||||
|
||||
bestTarget = (IAttackTarget)GenClosest.ClosestThing_Global(
|
||||
searcherThing.Position,
|
||||
tmpTargets,
|
||||
maxDist,
|
||||
reachableValidator,
|
||||
null,
|
||||
false);
|
||||
}
|
||||
|
||||
tmpTargets.Clear();
|
||||
return bestTarget;
|
||||
}
|
||||
/// <summary>
|
||||
/// 检查是否应该忽略非战斗人员
|
||||
/// </summary>
|
||||
private static bool ShouldIgnoreNoncombatant(Thing searcherThing, IAttackTarget target, TargetScanFlags flags)
|
||||
{
|
||||
// 只对Pawn类型的目标进行判断
|
||||
if (!(target is Pawn pawn))
|
||||
return false;
|
||||
|
||||
// 如果是战斗人员,不忽略
|
||||
if (pawn.IsCombatant())
|
||||
return false;
|
||||
|
||||
// 如果设置了忽略非战斗人员标志,则忽略
|
||||
if ((flags & TargetScanFlags.IgnoreNonCombatants) > TargetScanFlags.None)
|
||||
return true;
|
||||
|
||||
// 如果看不到非战斗人员,则忽略
|
||||
return !GenSight.LineOfSightToThing(searcherThing.Position, pawn, searcherThing.Map, false, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以从当前位置射击目标
|
||||
/// </summary>
|
||||
private static bool CanShootAtFromCurrentPosition(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb)
|
||||
{
|
||||
return verb != null && verb.CanHitTargetFrom(searcher.Thing.Position, target.Thing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过权重随机获取射击目标
|
||||
/// </summary>
|
||||
private static IAttackTarget GetRandomShootingTargetByScore(List<IAttackTarget> targets, IAttackTargetSearcher searcher, Verb verb, Vector3 angle)
|
||||
{
|
||||
var availableTargets = GetAvailableShootingTargetsByScore(targets, searcher, verb, angle);
|
||||
if (availableTargets.TryRandomElementByWeight(x => x.Second, out Pair<IAttackTarget, float> result))
|
||||
{
|
||||
return result.First;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用射击目标及其分数的列表
|
||||
/// </summary>
|
||||
private static List<Pair<IAttackTarget, float>> GetAvailableShootingTargetsByScore(
|
||||
List<IAttackTarget> rawTargets,
|
||||
IAttackTargetSearcher searcher,
|
||||
Verb verb,
|
||||
Vector3 angle)
|
||||
{
|
||||
availableShootingTargets.Clear();
|
||||
|
||||
if (rawTargets.Count == 0)
|
||||
return availableShootingTargets;
|
||||
|
||||
// 初始化临时列表
|
||||
tmpTargetScores.Clear();
|
||||
tmpCanShootAtTarget.Clear();
|
||||
|
||||
float highestScore = float.MinValue;
|
||||
IAttackTarget bestTarget = null;
|
||||
|
||||
// 第一轮遍历:计算基础分数并标记可射击目标
|
||||
for (int i = 0; i < rawTargets.Count; i++)
|
||||
{
|
||||
tmpTargetScores.Add(float.MinValue);
|
||||
tmpCanShootAtTarget.Add(false);
|
||||
|
||||
// 跳过搜索者自身
|
||||
if (rawTargets[i] == searcher)
|
||||
continue;
|
||||
|
||||
// 检查是否可以射击
|
||||
bool canShoot = CanShootAtFromCurrentPosition(rawTargets[i], searcher, verb);
|
||||
tmpCanShootAtTarget[i] = canShoot;
|
||||
|
||||
if (canShoot)
|
||||
{
|
||||
// 计算射击目标分数
|
||||
float score = GetShootingTargetScore(rawTargets[i], searcher, verb, angle);
|
||||
tmpTargetScores[i] = score;
|
||||
|
||||
// 更新最佳目标
|
||||
if (bestTarget == null || score > highestScore)
|
||||
{
|
||||
bestTarget = rawTargets[i];
|
||||
highestScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建可用目标列表
|
||||
for (int j = 0; j < rawTargets.Count; j++)
|
||||
{
|
||||
if (rawTargets[j] != searcher && tmpCanShootAtTarget[j])
|
||||
{
|
||||
availableShootingTargets.Add(new Pair<IAttackTarget, float>(rawTargets[j], tmpTargetScores[j]));
|
||||
}
|
||||
}
|
||||
|
||||
return availableShootingTargets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算射击目标分数(核心评分算法)
|
||||
/// </summary>
|
||||
private static float GetShootingTargetScore(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb, Vector3 angle)
|
||||
{
|
||||
float score = 60f; // 基础分数
|
||||
|
||||
// 距离因素:越近分数越高(最多40分)
|
||||
float distance = (target.Thing.Position - searcher.Thing.Position).LengthHorizontal;
|
||||
score -= Mathf.Min(distance, 40f);
|
||||
|
||||
// 目标正在瞄准自己:加分
|
||||
if (target.TargetCurrentlyAimingAt == searcher.Thing)
|
||||
score += 10f;
|
||||
|
||||
// 最近攻击目标:加分(如果最近攻击过这个目标)
|
||||
if (searcher.LastAttackedTarget == target.Thing && Find.TickManager.TicksGame - searcher.LastAttackTargetTick <= 300)
|
||||
score += 40f;
|
||||
|
||||
// 掩体因素:目标有掩体保护则减分
|
||||
float blockChance = CoverUtility.CalculateOverallBlockChance(target.Thing.Position, searcher.Thing.Position, searcher.Thing.Map);
|
||||
score -= blockChance * 10f;
|
||||
|
||||
// Pawn特定因素
|
||||
if (target is Pawn pawnTarget)
|
||||
{
|
||||
// 非战斗人员减分
|
||||
score -= NonCombatantScore(pawnTarget);
|
||||
|
||||
// 远程攻击目标特殊处理
|
||||
if (verb.verbProps.ai_TargetHasRangedAttackScoreOffset != 0f &&
|
||||
pawnTarget.CurrentEffectiveVerb != null &&
|
||||
pawnTarget.CurrentEffectiveVerb.verbProps.Ranged)
|
||||
{
|
||||
score += verb.verbProps.ai_TargetHasRangedAttackScoreOffset;
|
||||
}
|
||||
|
||||
// 倒地目标大幅减分
|
||||
if (pawnTarget.Downed)
|
||||
score -= 50f;
|
||||
}
|
||||
|
||||
// 友军误伤因素
|
||||
score += FriendlyFireBlastRadiusTargetScoreOffset(target, searcher, verb);
|
||||
score += FriendlyFireConeTargetScoreOffset(target, searcher, verb);
|
||||
|
||||
// 角度因素:计算与理想角度的偏差
|
||||
Vector3 targetDirection = (target.Thing.DrawPos - searcher.Thing.DrawPos).Yto0();
|
||||
float angleDeviation = Vector3.Angle(angle, targetDirection);
|
||||
|
||||
// 防止除零错误
|
||||
if (angleDeviation < 0.1f)
|
||||
angleDeviation = 0.1f;
|
||||
|
||||
// 最终分数计算:考虑目标优先级因子和角度偏差
|
||||
float finalScore = score * target.TargetPriorityFactor / angleDeviation;
|
||||
|
||||
// 确保返回正数
|
||||
return Mathf.Max(finalScore, 0.01f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算非战斗人员分数
|
||||
/// </summary>
|
||||
private static float NonCombatantScore(Thing target)
|
||||
{
|
||||
if (!(target is Pawn pawn))
|
||||
return 0f;
|
||||
|
||||
if (!pawn.IsCombatant())
|
||||
return 50f; // 非战斗人员大幅减分
|
||||
|
||||
if (pawn.DevelopmentalStage.Juvenile())
|
||||
return 25f; // 未成年人中等减分
|
||||
|
||||
return 0f; // 战斗成年人不减分
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算爆炸半径内的友军误伤分数偏移
|
||||
/// </summary>
|
||||
private static float FriendlyFireBlastRadiusTargetScoreOffset(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb)
|
||||
{
|
||||
// 检查是否启用了避免友军误伤半径
|
||||
if (verb.verbProps.ai_AvoidFriendlyFireRadius <= 0f)
|
||||
return 0f;
|
||||
|
||||
Map map = target.Thing.Map;
|
||||
IntVec3 targetPosition = target.Thing.Position;
|
||||
int cellCount = GenRadial.NumCellsInRadius(verb.verbProps.ai_AvoidFriendlyFireRadius);
|
||||
float friendlyFireScore = 0f;
|
||||
|
||||
// 遍历爆炸半径内的所有单元格
|
||||
for (int i = 0; i < cellCount; i++)
|
||||
{
|
||||
IntVec3 checkCell = targetPosition + GenRadial.RadialPattern[i];
|
||||
|
||||
if (!checkCell.InBounds(map))
|
||||
continue;
|
||||
|
||||
bool hasLineOfSight = true;
|
||||
List<Thing> thingsInCell = checkCell.GetThingList(map);
|
||||
|
||||
// 检查单元格内的所有物体
|
||||
for (int j = 0; j < thingsInCell.Count; j++)
|
||||
{
|
||||
Thing thing = thingsInCell[j];
|
||||
|
||||
// 只关心攻击目标且不是当前目标
|
||||
if (!(thing is IAttackTarget) || thing == target)
|
||||
continue;
|
||||
|
||||
// 检查视线(只检查一次)
|
||||
if (hasLineOfSight)
|
||||
{
|
||||
if (!GenSight.LineOfSight(targetPosition, checkCell, map, true, null, 0, 0))
|
||||
break; // 没有视线,跳过这个单元格
|
||||
|
||||
hasLineOfSight = false;
|
||||
}
|
||||
|
||||
// 计算误伤分数
|
||||
float hitScore;
|
||||
if (thing == searcher)
|
||||
hitScore = FriendlyFireScoreOffsetSelf; // 击中自己
|
||||
else if (!(thing is Pawn))
|
||||
hitScore = FriendlyFireScoreOffsetPerNonPawn; // 非Pawn物体
|
||||
else if (thing.def.race.Animal)
|
||||
hitScore = FriendlyFireScoreOffsetPerAnimal; // 动物
|
||||
else
|
||||
hitScore = FriendlyFireScoreOffsetPerHumanlikeOrMechanoid; // 人类或机械族
|
||||
|
||||
// 根据敌对关系调整分数
|
||||
if (!searcher.Thing.HostileTo(thing))
|
||||
friendlyFireScore -= hitScore; // 友军:减分
|
||||
else
|
||||
friendlyFireScore += hitScore * 0.6f; // 敌军:小幅加分
|
||||
}
|
||||
}
|
||||
|
||||
return friendlyFireScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算锥形范围内的友军误伤分数偏移
|
||||
/// </summary>
|
||||
private static float FriendlyFireConeTargetScoreOffset(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb)
|
||||
{
|
||||
// 只对Pawn类型的搜索者进行计算
|
||||
if (!(searcher.Thing is Pawn searcherPawn))
|
||||
return 0f;
|
||||
|
||||
// 检查智能等级
|
||||
if (searcherPawn.RaceProps.intelligence < Intelligence.ToolUser)
|
||||
return 0f;
|
||||
|
||||
// 机械族不计算锥形误伤
|
||||
if (searcherPawn.RaceProps.IsMechanoid)
|
||||
return 0f;
|
||||
|
||||
// 只处理射击类动词
|
||||
if (!(verb is Verb_Shoot shootVerb))
|
||||
return 0f;
|
||||
|
||||
ThingDef projectileDef = shootVerb.verbProps.defaultProjectile;
|
||||
if (projectileDef == null)
|
||||
return 0f;
|
||||
|
||||
// 高空飞行的抛射物不计算锥形误伤
|
||||
if (projectileDef.projectile.flyOverhead)
|
||||
return 0f;
|
||||
|
||||
Map map = searcherPawn.Map;
|
||||
|
||||
// 获取射击报告
|
||||
ShotReport report = ShotReport.HitReportFor(searcherPawn, verb, (Thing)target);
|
||||
|
||||
// 计算强制失误半径
|
||||
float forcedMissRadius = Mathf.Max(
|
||||
VerbUtility.CalculateAdjustedForcedMiss(verb.verbProps.ForcedMissRadius, report.ShootLine.Dest - report.ShootLine.Source),
|
||||
1.5f);
|
||||
|
||||
// 获取可能被误伤的所有单元格
|
||||
IEnumerable<IntVec3> potentialHitCells =
|
||||
from dest in GenRadial.RadialCellsAround(report.ShootLine.Dest, forcedMissRadius, true)
|
||||
where dest.InBounds(map)
|
||||
select new ShootLine(report.ShootLine.Source, dest)
|
||||
into line
|
||||
from pos in line.Points().Concat(line.Dest).TakeWhile(pos => pos.CanBeSeenOverFast(map))
|
||||
select pos;
|
||||
|
||||
potentialHitCells = potentialHitCells.Distinct();
|
||||
|
||||
float coneFriendlyFireScore = 0f;
|
||||
|
||||
// 计算锥形范围内的误伤分数
|
||||
foreach (IntVec3 cell in potentialHitCells)
|
||||
{
|
||||
float interceptChance = VerbUtility.InterceptChanceFactorFromDistance(report.ShootLine.Source.ToVector3Shifted(), cell);
|
||||
|
||||
if (interceptChance <= 0f)
|
||||
continue;
|
||||
|
||||
List<Thing> thingsInCell = cell.GetThingList(map);
|
||||
|
||||
for (int i = 0; i < thingsInCell.Count; i++)
|
||||
{
|
||||
Thing thing = thingsInCell[i];
|
||||
|
||||
if (!(thing is IAttackTarget) || thing == target)
|
||||
continue;
|
||||
|
||||
// 计算误伤分数
|
||||
float hitScore;
|
||||
if (thing == searcher)
|
||||
hitScore = FriendlyFireScoreOffsetSelf;
|
||||
else if (!(thing is Pawn))
|
||||
hitScore = FriendlyFireScoreOffsetPerNonPawn;
|
||||
else if (thing.def.race.Animal)
|
||||
hitScore = FriendlyFireScoreOffsetPerAnimal;
|
||||
else
|
||||
hitScore = FriendlyFireScoreOffsetPerHumanlikeOrMechanoid;
|
||||
|
||||
// 根据拦截概率和敌对关系调整分数
|
||||
hitScore *= interceptChance;
|
||||
if (!searcher.Thing.HostileTo(thing))
|
||||
hitScore = -hitScore; // 友军:减分
|
||||
else
|
||||
hitScore *= 0.6f; // 敌军:小幅加分
|
||||
|
||||
coneFriendlyFireScore += hitScore;
|
||||
}
|
||||
}
|
||||
|
||||
return coneFriendlyFireScore;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user