整理scoure
This commit is contained in:
BIN
1.6/1.6/Assemblies/AlienRace.dll
Normal file
BIN
1.6/1.6/Assemblies/AlienRace.dll
Normal file
Binary file not shown.
Binary file not shown.
@@ -317,7 +317,7 @@
|
||||
</li>
|
||||
</stages>
|
||||
<comps>
|
||||
<li Class="WulaFallenEmpire.MoharHediffs.HediffCompProperties_Spawner">
|
||||
<li Class="WulaFallenEmpire.HediffCompProperties_Spawner">
|
||||
<!--
|
||||
==================================================
|
||||
基础设置 (Basic Settings)
|
||||
|
||||
700
Source/WulaFallenEmpire/Flyover/ThingclassFlyOver.cs
Normal file
700
Source/WulaFallenEmpire/Flyover/ThingclassFlyOver.cs
Normal file
@@ -0,0 +1,700 @@
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.Sound;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
[StaticConstructorOnStartup]
|
||||
public class FlyOver : ThingWithComps, IThingHolder
|
||||
{
|
||||
// 核心字段
|
||||
public ThingOwner innerContainer; // 内部物品容器
|
||||
public IntVec3 startPosition; // 起始位置
|
||||
public IntVec3 endPosition; // 结束位置
|
||||
public float flightSpeed = 1f; // 飞行速度
|
||||
public float currentProgress = 0f; // 当前进度 (0-1)
|
||||
public float altitude = 10f; // 飞行高度
|
||||
public Faction faction; // 派系引用
|
||||
|
||||
// 淡入效果相关
|
||||
public float fadeInDuration = 1.5f; // 淡入持续时间(秒)
|
||||
public float currentFadeInTime = 0f; // 当前淡入时间
|
||||
public bool fadeInCompleted = false; // 淡入是否完成
|
||||
|
||||
// 淡出效果相关
|
||||
public float fadeOutDuration = 0f; // 动态计算的淡出持续时间
|
||||
public float currentFadeOutTime = 0f; // 当前淡出时间
|
||||
public bool fadeOutStarted = false; // 淡出是否开始
|
||||
public bool fadeOutCompleted = false; // 淡出是否完成
|
||||
public float fadeOutStartProgress = 0.7f; // 开始淡出的进度阈值(0-1)
|
||||
public float defaultFadeOutDuration = 1.5f; // 默认淡出持续时间(仅用于销毁)
|
||||
|
||||
// 进场动画相关 - 新增
|
||||
public float approachDuration = 1.0f; // 进场动画持续时间(秒)
|
||||
public float currentApproachTime = 0f; // 当前进场动画时间
|
||||
public bool approachCompleted = false; // 进场动画是否完成
|
||||
public float approachOffsetDistance = 3f; // 进场偏移距离(格)
|
||||
public bool useApproachAnimation = true; // 是否使用进场动画
|
||||
|
||||
// 伴飞相关
|
||||
public float escortScale = 1f; // 伴飞缩放比例
|
||||
public bool isEscort = false; // 是否是伴飞
|
||||
|
||||
// 状态标志
|
||||
public bool hasStarted = false;
|
||||
public bool hasCompleted = false;
|
||||
|
||||
// 音效系统
|
||||
private Sustainer flightSoundPlaying;
|
||||
|
||||
// 视觉效果
|
||||
private Material cachedShadowMaterial;
|
||||
private static MaterialPropertyBlock shadowPropertyBlock = new MaterialPropertyBlock();
|
||||
private static MaterialPropertyBlock fadePropertyBlock = new MaterialPropertyBlock();
|
||||
|
||||
// 配置字段
|
||||
public bool spawnContentsOnImpact = false; // 是否在结束时生成内容物
|
||||
public bool playFlyOverSound = true; // 是否播放飞越音效
|
||||
public bool createShadow = true; // 是否创建阴影
|
||||
|
||||
public Pawn caster; // 施法者引用
|
||||
|
||||
// 属性 - 修改后的 DrawPos,包含进场动画
|
||||
public override Vector3 DrawPos
|
||||
{
|
||||
get
|
||||
{
|
||||
// 线性插值计算基础位置
|
||||
Vector3 start = startPosition.ToVector3();
|
||||
Vector3 end = endPosition.ToVector3();
|
||||
Vector3 basePos = Vector3.Lerp(start, end, currentProgress);
|
||||
|
||||
// 添加高度偏移
|
||||
basePos.y = altitude;
|
||||
|
||||
// 应用进场动画偏移
|
||||
if (useApproachAnimation && !approachCompleted)
|
||||
{
|
||||
basePos = ApplyApproachAnimation(basePos);
|
||||
}
|
||||
|
||||
return basePos;
|
||||
}
|
||||
}
|
||||
|
||||
// 进场动画位置计算
|
||||
private Vector3 ApplyApproachAnimation(Vector3 basePos)
|
||||
{
|
||||
float approachProgress = currentApproachTime / approachDuration;
|
||||
|
||||
// 使用缓动函数让移动更自然
|
||||
float easedProgress = EasingFunction(approachProgress, EasingType.OutCubic);
|
||||
|
||||
// 计算偏移方向(飞行方向的反方向)
|
||||
Vector3 approachDirection = -MovementDirection.normalized;
|
||||
|
||||
// 计算偏移量:从最大偏移逐渐减少到0
|
||||
float currentOffset = approachOffsetDistance * (1f - easedProgress);
|
||||
|
||||
// 应用偏移
|
||||
Vector3 offsetPos = basePos + approachDirection * currentOffset;
|
||||
|
||||
return offsetPos;
|
||||
}
|
||||
|
||||
// 缓动函数 - 让动画更自然
|
||||
private float EasingFunction(float t, EasingType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case EasingType.OutCubic:
|
||||
return 1f - Mathf.Pow(1f - t, 3f);
|
||||
case EasingType.OutQuad:
|
||||
return 1f - (1f - t) * (1f - t);
|
||||
case EasingType.OutSine:
|
||||
return Mathf.Sin(t * Mathf.PI * 0.5f);
|
||||
default:
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
// 缓动类型枚举
|
||||
private enum EasingType
|
||||
{
|
||||
Linear,
|
||||
OutQuad,
|
||||
OutCubic,
|
||||
OutSine
|
||||
}
|
||||
|
||||
// 新增:进场动画进度(0-1)
|
||||
public float ApproachProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
if (approachCompleted) return 1f;
|
||||
return Mathf.Clamp01(currentApproachTime / approachDuration);
|
||||
}
|
||||
}
|
||||
|
||||
public override Graphic Graphic
|
||||
{
|
||||
get
|
||||
{
|
||||
Thing thingForGraphic = GetThingForGraphic();
|
||||
if (thingForGraphic == this)
|
||||
{
|
||||
return base.Graphic;
|
||||
}
|
||||
return thingForGraphic.Graphic.ExtractInnerGraphicFor(thingForGraphic);
|
||||
}
|
||||
}
|
||||
|
||||
protected Material ShadowMaterial
|
||||
{
|
||||
get
|
||||
{
|
||||
if (cachedShadowMaterial == null && createShadow)
|
||||
{
|
||||
cachedShadowMaterial = MaterialPool.MatFrom("Things/Skyfaller/SkyfallerShadowCircle", ShaderDatabase.Transparent);
|
||||
}
|
||||
return cachedShadowMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
// 精确旋转 - 模仿原版 Projectile
|
||||
public virtual Quaternion ExactRotation
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector3 direction = (endPosition.ToVector3() - startPosition.ToVector3()).normalized;
|
||||
return Quaternion.LookRotation(direction.Yto0());
|
||||
}
|
||||
}
|
||||
|
||||
// 简化的方向计算方法
|
||||
public Vector3 MovementDirection
|
||||
{
|
||||
get
|
||||
{
|
||||
return (endPosition.ToVector3() - startPosition.ToVector3()).normalized;
|
||||
}
|
||||
}
|
||||
|
||||
// 淡入透明度(0-1)
|
||||
public float FadeInAlpha
|
||||
{
|
||||
get
|
||||
{
|
||||
if (fadeInCompleted) return 1f;
|
||||
return Mathf.Clamp01(currentFadeInTime / fadeInDuration);
|
||||
}
|
||||
}
|
||||
|
||||
// 淡出透明度(0-1)
|
||||
public float FadeOutAlpha
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!fadeOutStarted) return 1f;
|
||||
if (fadeOutCompleted) return 0f;
|
||||
return Mathf.Clamp01(1f - (currentFadeOutTime / fadeOutDuration));
|
||||
}
|
||||
}
|
||||
|
||||
// 总体透明度(淡入 * 淡出)
|
||||
public float OverallAlpha
|
||||
{
|
||||
get
|
||||
{
|
||||
return FadeInAlpha * FadeOutAlpha;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:计算剩余飞行时间(秒)
|
||||
public float RemainingFlightTime
|
||||
{
|
||||
get
|
||||
{
|
||||
float remainingProgress = 1f - currentProgress;
|
||||
return remainingProgress / (flightSpeed * 0.001f) * (1f / 60f);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:计算基于剩余距离的淡出持续时间
|
||||
private float CalculateDynamicFadeOutDuration()
|
||||
{
|
||||
// 获取 ModExtension 配置
|
||||
var shadowExtension = def.GetModExtension<FlyOverShadowExtension>();
|
||||
float minFadeOutDuration = shadowExtension?.minFadeOutDuration ?? 0.5f;
|
||||
float maxFadeOutDuration = shadowExtension?.maxFadeOutDuration ?? 3f;
|
||||
float fadeOutDistanceFactor = shadowExtension?.fadeOutDistanceFactor ?? 0.3f;
|
||||
|
||||
// 计算剩余飞行时间
|
||||
float remainingTime = RemainingFlightTime;
|
||||
|
||||
// 使用剩余时间的一部分作为淡出持续时间
|
||||
float dynamicDuration = remainingTime * fadeOutDistanceFactor;
|
||||
|
||||
// 限制在最小和最大范围内
|
||||
return Mathf.Clamp(dynamicDuration, minFadeOutDuration, maxFadeOutDuration);
|
||||
}
|
||||
|
||||
public FlyOver()
|
||||
{
|
||||
innerContainer = new ThingOwner<Thing>(this);
|
||||
}
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_Deep.Look(ref innerContainer, "innerContainer", this);
|
||||
Scribe_Values.Look(ref startPosition, "startPosition");
|
||||
Scribe_Values.Look(ref endPosition, "endPosition");
|
||||
Scribe_Values.Look(ref flightSpeed, "flightSpeed", 1f);
|
||||
Scribe_Values.Look(ref currentProgress, "currentProgress", 0f);
|
||||
Scribe_Values.Look(ref altitude, "altitude", 10f);
|
||||
Scribe_Values.Look(ref hasStarted, "hasStarted", false);
|
||||
Scribe_Values.Look(ref hasCompleted, "hasCompleted", false);
|
||||
Scribe_Values.Look(ref spawnContentsOnImpact, "spawnContentsOnImpact", false);
|
||||
Scribe_Values.Look(ref fadeInDuration, "fadeInDuration", 1.5f);
|
||||
Scribe_Values.Look(ref currentFadeInTime, "currentFadeInTime", 0f);
|
||||
Scribe_Values.Look(ref fadeInCompleted, "fadeInCompleted", false);
|
||||
|
||||
// 淡出效果数据保存
|
||||
Scribe_Values.Look(ref fadeOutDuration, "fadeOutDuration", 0f);
|
||||
Scribe_Values.Look(ref currentFadeOutTime, "currentFadeOutTime", 0f);
|
||||
Scribe_Values.Look(ref fadeOutStarted, "fadeOutStarted", false);
|
||||
Scribe_Values.Look(ref fadeOutCompleted, "fadeOutCompleted", false);
|
||||
Scribe_Values.Look(ref fadeOutStartProgress, "fadeOutStartProgress", 0.7f);
|
||||
Scribe_Values.Look(ref defaultFadeOutDuration, "defaultFadeOutDuration", 1.5f);
|
||||
|
||||
// 进场动画数据保存 - 新增
|
||||
Scribe_Values.Look(ref approachDuration, "approachDuration", 1.0f);
|
||||
Scribe_Values.Look(ref currentApproachTime, "currentApproachTime", 0f);
|
||||
Scribe_Values.Look(ref approachCompleted, "approachCompleted", false);
|
||||
Scribe_Values.Look(ref approachOffsetDistance, "approachOffsetDistance", 3f);
|
||||
Scribe_Values.Look(ref useApproachAnimation, "useApproachAnimation", true);
|
||||
|
||||
Scribe_References.Look(ref caster, "caster");
|
||||
Scribe_References.Look(ref faction, "faction");
|
||||
}
|
||||
|
||||
public override void SpawnSetup(Map map, bool respawningAfterLoad)
|
||||
{
|
||||
base.SpawnSetup(map, respawningAfterLoad);
|
||||
|
||||
Log.Message($"FlyOver Spawned - Start: {startPosition}, End: {endPosition}, Speed: {flightSpeed}, Altitude: {altitude}");
|
||||
if (!respawningAfterLoad)
|
||||
{
|
||||
Log.Message($"FlyOver Direction - Vector: {MovementDirection}, Rotation: {ExactRotation.eulerAngles}");
|
||||
|
||||
// 设置初始位置
|
||||
base.Position = startPosition;
|
||||
hasStarted = true;
|
||||
|
||||
// 重置淡入状态
|
||||
currentFadeInTime = 0f;
|
||||
fadeInCompleted = false;
|
||||
|
||||
// 重置淡出状态
|
||||
currentFadeOutTime = 0f;
|
||||
fadeOutStarted = false;
|
||||
fadeOutCompleted = false;
|
||||
fadeOutDuration = 0f;
|
||||
|
||||
// 重置进场动画状态 - 新增
|
||||
currentApproachTime = 0f;
|
||||
approachCompleted = false;
|
||||
|
||||
// 从 ModExtension 加载进场动画配置
|
||||
var extension = def.GetModExtension<FlyOverShadowExtension>();
|
||||
if (extension != null)
|
||||
{
|
||||
useApproachAnimation = extension.useApproachAnimation;
|
||||
approachDuration = extension.approachDuration;
|
||||
approachOffsetDistance = extension.approachOffsetDistance;
|
||||
}
|
||||
|
||||
Log.Message($"FlyOver approach animation: {useApproachAnimation}, duration: {approachDuration}s, offset: {approachOffsetDistance}");
|
||||
|
||||
// 开始飞行音效
|
||||
if (playFlyOverSound && def.skyfaller?.floatingSound != null)
|
||||
{
|
||||
flightSoundPlaying = def.skyfaller.floatingSound.TrySpawnSustainer(
|
||||
SoundInfo.InMap(new TargetInfo(startPosition, map), MaintenanceType.PerTick));
|
||||
Log.Message("FlyOver sound started");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Tick()
|
||||
{
|
||||
base.Tick();
|
||||
|
||||
if (!hasStarted || hasCompleted)
|
||||
return;
|
||||
|
||||
// 更新进场动画 - 新增
|
||||
if (useApproachAnimation && !approachCompleted)
|
||||
{
|
||||
currentApproachTime += 1f / 60f;
|
||||
if (currentApproachTime >= approachDuration)
|
||||
{
|
||||
approachCompleted = true;
|
||||
currentApproachTime = approachDuration;
|
||||
Log.Message("FlyOver approach animation completed");
|
||||
}
|
||||
}
|
||||
|
||||
// 更新淡入效果
|
||||
if (!fadeInCompleted)
|
||||
{
|
||||
currentFadeInTime += 1f / 60f;
|
||||
if (currentFadeInTime >= fadeInDuration)
|
||||
{
|
||||
fadeInCompleted = true;
|
||||
currentFadeInTime = fadeInDuration;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新飞行进度
|
||||
currentProgress += flightSpeed * 0.001f;
|
||||
|
||||
// 检查是否应该开始淡出(基于剩余距离动态计算)
|
||||
if (!fadeOutStarted && currentProgress >= fadeOutStartProgress)
|
||||
{
|
||||
StartFadeOut();
|
||||
}
|
||||
|
||||
// 更新淡出效果
|
||||
if (fadeOutStarted && !fadeOutCompleted)
|
||||
{
|
||||
currentFadeOutTime += 1f / 60f;
|
||||
if (currentFadeOutTime >= fadeOutDuration)
|
||||
{
|
||||
fadeOutCompleted = true;
|
||||
currentFadeOutTime = fadeOutDuration;
|
||||
Log.Message("FlyOver fade out completed");
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前位置
|
||||
UpdatePosition();
|
||||
|
||||
// 维持飞行音效(在淡出时逐渐降低音量)
|
||||
UpdateFlightSound();
|
||||
|
||||
// 检查是否到达终点
|
||||
if (currentProgress >= 1f)
|
||||
{
|
||||
CompleteFlyOver();
|
||||
}
|
||||
|
||||
// 生成飞行轨迹特效(在淡出时减少特效)
|
||||
CreateFlightEffects();
|
||||
}
|
||||
|
||||
// 新增:开始淡出效果
|
||||
private void StartFadeOut()
|
||||
{
|
||||
fadeOutStarted = true;
|
||||
|
||||
// 基于剩余距离动态计算淡出持续时间
|
||||
fadeOutDuration = CalculateDynamicFadeOutDuration();
|
||||
|
||||
Log.Message($"FlyOver started fade out at progress {currentProgress:F2}, duration: {fadeOutDuration:F2}s, remaining time: {RemainingFlightTime:F2}s");
|
||||
}
|
||||
|
||||
private void UpdatePosition()
|
||||
{
|
||||
Vector3 currentWorldPos = Vector3.Lerp(startPosition.ToVector3(), endPosition.ToVector3(), currentProgress);
|
||||
IntVec3 newPos = currentWorldPos.ToIntVec3();
|
||||
|
||||
if (newPos != base.Position && newPos.InBounds(base.Map))
|
||||
{
|
||||
base.Position = newPos;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFlightSound()
|
||||
{
|
||||
if (flightSoundPlaying != null)
|
||||
{
|
||||
if (fadeOutStarted)
|
||||
{
|
||||
// 淡出时逐渐降低音效音量
|
||||
flightSoundPlaying.externalParams["VolumeFactor"] = FadeOutAlpha;
|
||||
}
|
||||
flightSoundPlaying?.Maintain();
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteFlyOver()
|
||||
{
|
||||
hasCompleted = true;
|
||||
currentProgress = 1f;
|
||||
|
||||
// 生成内容物(如果需要)
|
||||
if (spawnContentsOnImpact && innerContainer.Any)
|
||||
{
|
||||
SpawnContents();
|
||||
}
|
||||
|
||||
// 播放完成音效
|
||||
if (def.skyfaller?.impactSound != null)
|
||||
{
|
||||
def.skyfaller.impactSound.PlayOneShot(
|
||||
SoundInfo.InMap(new TargetInfo(endPosition, base.Map)));
|
||||
}
|
||||
|
||||
Log.Message($"FlyOver completed at {endPosition}");
|
||||
|
||||
// 销毁自身
|
||||
Destroy();
|
||||
}
|
||||
|
||||
// 新增:紧急销毁方法(使用默认淡出时间)
|
||||
public void EmergencyDestroy()
|
||||
{
|
||||
if (!fadeOutStarted)
|
||||
{
|
||||
// 如果还没有开始淡出,使用默认淡出时间
|
||||
fadeOutStarted = true;
|
||||
fadeOutDuration = defaultFadeOutDuration;
|
||||
Log.Message($"FlyOver emergency destroy with default fade out: {defaultFadeOutDuration}s");
|
||||
}
|
||||
|
||||
// 设置标记,下一帧会处理淡出
|
||||
hasCompleted = true;
|
||||
}
|
||||
|
||||
private void SpawnContents()
|
||||
{
|
||||
foreach (Thing thing in innerContainer)
|
||||
{
|
||||
if (thing != null && !thing.Destroyed)
|
||||
{
|
||||
GenPlace.TryPlaceThing(thing, endPosition, base.Map, ThingPlaceMode.Near);
|
||||
}
|
||||
}
|
||||
innerContainer.Clear();
|
||||
}
|
||||
|
||||
private void CreateFlightEffects()
|
||||
{
|
||||
// 在飞行轨迹上生成粒子效果
|
||||
if (Rand.MTBEventOccurs(0.5f, 1f, 1f) && def.skyfaller?.motesPerCell > 0 && !fadeOutCompleted)
|
||||
{
|
||||
Vector3 effectPos = DrawPos;
|
||||
effectPos.y = AltitudeLayer.MoteOverhead.AltitudeFor();
|
||||
|
||||
// 淡出时减少粒子效果强度
|
||||
float effectIntensity = fadeOutStarted ? FadeOutAlpha : 1f;
|
||||
FleckMaker.ThrowSmoke(effectPos, base.Map, 1f * effectIntensity);
|
||||
|
||||
// 可选:根据速度生成更多效果
|
||||
if (flightSpeed > 2f && !fadeOutStarted)
|
||||
{
|
||||
FleckMaker.ThrowAirPuffUp(effectPos, base.Map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关键修复:重写 DrawAt 方法,绕过探索状态检查
|
||||
protected override void DrawAt(Vector3 drawLoc, bool flip = false)
|
||||
{
|
||||
// 直接绘制,不检查探索状态
|
||||
Vector3 finalDrawPos = drawLoc;
|
||||
|
||||
if (createShadow)
|
||||
{
|
||||
DrawFlightShadow();
|
||||
}
|
||||
|
||||
DrawFlyOverWithFade(finalDrawPos);
|
||||
}
|
||||
|
||||
protected virtual void DrawFlyOverWithFade(Vector3 drawPos)
|
||||
{
|
||||
Thing thingForGraphic = GetThingForGraphic();
|
||||
Graphic graphic = thingForGraphic.Graphic;
|
||||
if (graphic == null)
|
||||
return;
|
||||
Material material = graphic.MatSingle;
|
||||
if (material == null)
|
||||
return;
|
||||
float alpha = OverallAlpha;
|
||||
if (alpha <= 0.001f)
|
||||
return;
|
||||
if (fadeInCompleted && !fadeOutStarted && alpha >= 0.999f)
|
||||
{
|
||||
Vector3 highAltitudePos = drawPos;
|
||||
highAltitudePos.y = AltitudeLayer.MetaOverlays.AltitudeFor();
|
||||
|
||||
// 应用伴飞缩放
|
||||
Vector3 finalScale = Vector3.one;
|
||||
if (def.graphicData != null)
|
||||
{
|
||||
finalScale = new Vector3(def.graphicData.drawSize.x * escortScale, 1f, def.graphicData.drawSize.y * escortScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
finalScale = new Vector3(escortScale, 1f, escortScale);
|
||||
}
|
||||
|
||||
Matrix4x4 matrix = Matrix4x4.TRS(highAltitudePos, ExactRotation, finalScale);
|
||||
Graphics.DrawMesh(MeshPool.plane10, matrix, material, 0);
|
||||
return;
|
||||
}
|
||||
fadePropertyBlock.SetColor(ShaderPropertyIDs.Color,
|
||||
new Color(graphic.Color.r, graphic.Color.g, graphic.Color.b, graphic.Color.a * alpha));
|
||||
// 应用伴飞缩放
|
||||
Vector3 scale = Vector3.one;
|
||||
if (def.graphicData != null)
|
||||
{
|
||||
scale = new Vector3(def.graphicData.drawSize.x * escortScale, 1f, def.graphicData.drawSize.y * escortScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
scale = new Vector3(escortScale, 1f, escortScale);
|
||||
}
|
||||
Vector3 highPos = drawPos;
|
||||
highPos.y = AltitudeLayer.MetaOverlays.AltitudeFor();
|
||||
Matrix4x4 matrix2 = Matrix4x4.TRS(highPos, ExactRotation, scale);
|
||||
Graphics.DrawMesh(MeshPool.plane10, matrix2, material, 0, null, 0, fadePropertyBlock);
|
||||
}
|
||||
|
||||
protected virtual void DrawFlightShadow()
|
||||
{
|
||||
var shadowExtension = def.GetModExtension<FlyOverShadowExtension>();
|
||||
|
||||
Material shadowMaterial;
|
||||
if (shadowExtension?.useCustomShadow == true && !shadowExtension.customShadowPath.NullOrEmpty())
|
||||
{
|
||||
shadowMaterial = MaterialPool.MatFrom(shadowExtension.customShadowPath, ShaderDatabase.Transparent);
|
||||
}
|
||||
else
|
||||
{
|
||||
shadowMaterial = ShadowMaterial;
|
||||
}
|
||||
|
||||
if (shadowMaterial == null)
|
||||
return;
|
||||
|
||||
Vector3 shadowPos = DrawPos;
|
||||
shadowPos.y = AltitudeLayer.Shadows.AltitudeFor();
|
||||
|
||||
float shadowIntensity = shadowExtension?.shadowIntensity ?? 1f;
|
||||
float minAlpha = shadowExtension?.minShadowAlpha ?? 0.3f;
|
||||
float maxAlpha = shadowExtension?.maxShadowAlpha ?? 1f;
|
||||
float minScale = shadowExtension?.minShadowScale ?? 0.5f;
|
||||
float maxScale = shadowExtension?.maxShadowScale ?? 1.5f;
|
||||
|
||||
float shadowAlpha = Mathf.Lerp(minAlpha, maxAlpha, currentProgress) * shadowIntensity;
|
||||
float shadowScale = Mathf.Lerp(minScale, maxScale, currentProgress);
|
||||
|
||||
shadowAlpha *= OverallAlpha;
|
||||
|
||||
if (shadowAlpha <= 0.001f)
|
||||
return;
|
||||
|
||||
Vector3 s = new Vector3(shadowScale, 1f, shadowScale);
|
||||
Vector3 vector = new Vector3(0f, -0.01f, 0f);
|
||||
Matrix4x4 matrix = Matrix4x4.TRS(shadowPos + vector, Quaternion.identity, s);
|
||||
|
||||
Graphics.DrawMesh(MeshPool.plane10, matrix, shadowMaterial, 0);
|
||||
}
|
||||
|
||||
// IThingHolder 接口实现
|
||||
public ThingOwner GetDirectlyHeldThings()
|
||||
{
|
||||
return innerContainer;
|
||||
}
|
||||
|
||||
public void GetChildHolders(List<IThingHolder> outChildren)
|
||||
{
|
||||
ThingOwnerUtility.AppendThingHoldersFromThings(outChildren, GetDirectlyHeldThings());
|
||||
}
|
||||
|
||||
private Thing GetThingForGraphic()
|
||||
{
|
||||
if (def.graphicData != null || !innerContainer.Any)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
return innerContainer[0];
|
||||
}
|
||||
|
||||
// 工具方法:创建飞越物体
|
||||
public static FlyOver MakeFlyOver(ThingDef flyOverDef, IntVec3 start, IntVec3 end, Map map,
|
||||
float speed = 1f, float height = 10f, ThingOwner contents = null,
|
||||
float fadeInDuration = 1.5f, float defaultFadeOutDuration = 1.5f, Pawn casterPawn = null,
|
||||
bool useApproachAnimation = true, float approachDuration = 1.0f, float approachOffsetDistance = 3f) // 新增参数
|
||||
{
|
||||
FlyOver flyOver = (FlyOver)ThingMaker.MakeThing(flyOverDef);
|
||||
flyOver.startPosition = start;
|
||||
flyOver.endPosition = end;
|
||||
flyOver.flightSpeed = speed;
|
||||
flyOver.altitude = height;
|
||||
flyOver.fadeInDuration = fadeInDuration;
|
||||
flyOver.defaultFadeOutDuration = defaultFadeOutDuration;
|
||||
flyOver.caster = casterPawn;
|
||||
|
||||
// 进场动画参数 - 新增
|
||||
flyOver.useApproachAnimation = useApproachAnimation;
|
||||
flyOver.approachDuration = approachDuration;
|
||||
flyOver.approachOffsetDistance = approachOffsetDistance;
|
||||
|
||||
// 简化派系设置 - 直接设置 faction 字段
|
||||
if (casterPawn != null && casterPawn.Faction != null)
|
||||
{
|
||||
flyOver.faction = casterPawn.Faction;
|
||||
Log.Message($"FlyOver faction set to: {casterPawn.Faction.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"FlyOver: Cannot set faction - casterPawn: {casterPawn?.Label ?? "NULL"}, casterFaction: {casterPawn?.Faction?.Name ?? "NULL"}");
|
||||
}
|
||||
|
||||
if (contents != null)
|
||||
{
|
||||
flyOver.innerContainer.TryAddRangeOrTransfer(contents);
|
||||
}
|
||||
|
||||
GenSpawn.Spawn(flyOver, start, map);
|
||||
|
||||
Log.Message($"FlyOver created: {flyOver} from {start} to {end} at altitude {height}, Faction: {flyOver.faction?.Name ?? "NULL"}");
|
||||
return flyOver;
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展的 ModExtension 配置 - 新增进场动画参数
|
||||
public class FlyOverShadowExtension : DefModExtension
|
||||
{
|
||||
public string customShadowPath;
|
||||
public float shadowIntensity = 0.6f;
|
||||
public bool useCustomShadow = false;
|
||||
public float minShadowAlpha = 0.05f;
|
||||
public float maxShadowAlpha = 0.2f;
|
||||
public float minShadowScale = 0.5f;
|
||||
public float maxShadowScale = 1.0f;
|
||||
public float defaultFadeInDuration = 1.5f;
|
||||
public float defaultFadeOutDuration = 0.5f;
|
||||
public float fadeOutStartProgress = 0.98f;
|
||||
|
||||
// 动态淡出配置
|
||||
public float minFadeOutDuration = 0.5f;
|
||||
public float maxFadeOutDuration = 0.5f;
|
||||
public float fadeOutDistanceFactor = 0.01f;
|
||||
|
||||
public float ActuallyHeight = 150f;
|
||||
|
||||
// 进场动画配置 - 新增
|
||||
public bool useApproachAnimation = true;
|
||||
public float approachDuration = 1.0f;
|
||||
public float approachOffsetDistance = 3f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using RimWorld.Planet;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_AircraftStrike : CompProperties_AbilityEffect
|
||||
{
|
||||
public ThingDef requiredAircraftType; // 需要的战机类型
|
||||
public int aircraftCooldownTicks = 60000; // 战机冷却时间(默认1天)
|
||||
public int aircraftsPerUse = 1; // 每次使用消耗的战机数量
|
||||
|
||||
public CompProperties_AircraftStrike()
|
||||
{
|
||||
compClass = typeof(CompAbilityEffect_AircraftStrike);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompAbilityEffect_AircraftStrike : CompAbilityEffect
|
||||
{
|
||||
public new CompProperties_AircraftStrike Props => (CompProperties_AircraftStrike)props;
|
||||
|
||||
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
|
||||
{
|
||||
base.Apply(target, dest);
|
||||
|
||||
// 获取全局战机管理器
|
||||
WorldComponent_AircraftManager aircraftManager = Find.World.GetComponent<WorldComponent_AircraftManager>();
|
||||
|
||||
if (aircraftManager == null)
|
||||
{
|
||||
Log.Error("AircraftManagerNotFound".Translate());
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查并消耗战机
|
||||
if (aircraftManager.TryUseAircraft(Props.requiredAircraftType, Props.aircraftsPerUse, parent.pawn.Faction, Props.aircraftCooldownTicks))
|
||||
{
|
||||
// 成功消耗战机,发送消息
|
||||
Messages.Message("AircraftStrikeInitiated".Translate(Props.requiredAircraftType.LabelCap), MessageTypeDefOf.PositiveEvent);
|
||||
Log.Message("AircraftStrikeSuccess".Translate(Props.aircraftsPerUse, Props.requiredAircraftType.LabelCap));
|
||||
}
|
||||
else
|
||||
{
|
||||
Messages.Message("NoAvailableAircraft".Translate(Props.requiredAircraftType.LabelCap), MessageTypeDefOf.NegativeEvent);
|
||||
Log.Warning("AircraftStrikeFailed".Translate(Props.requiredAircraftType.LabelCap, parent.pawn.Faction?.Name ?? "UnknownFaction".Translate()));
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanApplyOn(LocalTargetInfo target, LocalTargetInfo dest)
|
||||
{
|
||||
// 检查是否有可用的战机
|
||||
WorldComponent_AircraftManager aircraftManager = Find.World.GetComponent<WorldComponent_AircraftManager>();
|
||||
|
||||
return base.CanApplyOn(target, dest) &&
|
||||
aircraftManager != null &&
|
||||
aircraftManager.HasAvailableAircraft(Props.requiredAircraftType, Props.aircraftsPerUse, parent.pawn.Faction);
|
||||
}
|
||||
|
||||
public override string ExtraLabelMouseAttachment(LocalTargetInfo target)
|
||||
{
|
||||
WorldComponent_AircraftManager aircraftManager = Find.World.GetComponent<WorldComponent_AircraftManager>();
|
||||
|
||||
if (aircraftManager != null)
|
||||
{
|
||||
int available = aircraftManager.GetAvailableAircraftCount(Props.requiredAircraftType, parent.pawn.Faction);
|
||||
int onCooldown = aircraftManager.GetCooldownAircraftCount(Props.requiredAircraftType, parent.pawn.Faction);
|
||||
|
||||
// 使用符号显示飞机状态
|
||||
string availableSymbols = GetAircraftSymbols(available, "◆");
|
||||
string cooldownSymbols = GetAircraftSymbols(onCooldown, "◇");
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine("AvailableAircraft".Translate(Props.requiredAircraftType.LabelCap, availableSymbols));
|
||||
sb.AppendLine("CooldownAircraft".Translate(cooldownSymbols));
|
||||
sb.Append("CostPerUse".Translate(Props.aircraftsPerUse));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
return base.ExtraLabelMouseAttachment(target);
|
||||
}
|
||||
|
||||
// 生成飞机符号表示
|
||||
private string GetAircraftSymbols(int count, string symbol)
|
||||
{
|
||||
if (count <= 0) return "—"; // 无飞机时显示破折号
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int displayCount = count;
|
||||
|
||||
// 如果数量过多,用数字+符号表示
|
||||
if (count > 10)
|
||||
{
|
||||
return $"{count}{symbol}";
|
||||
}
|
||||
|
||||
// 直接显示符号
|
||||
for (int i = 0; i < displayCount; i++)
|
||||
{
|
||||
sb.Append(symbol);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public override bool Valid(LocalTargetInfo target, bool throwMessages = false)
|
||||
{
|
||||
if (!base.Valid(target, throwMessages))
|
||||
return false;
|
||||
|
||||
// 检查战机可用性
|
||||
WorldComponent_AircraftManager aircraftManager = Find.World.GetComponent<WorldComponent_AircraftManager>();
|
||||
if (aircraftManager == null || !aircraftManager.HasAvailableAircraft(Props.requiredAircraftType, Props.aircraftsPerUse, parent.pawn.Faction))
|
||||
{
|
||||
if (throwMessages)
|
||||
{
|
||||
Messages.Message("NoAircraftForStrike".Translate(Props.requiredAircraftType.LabelCap), MessageTypeDefOf.RejectInput);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 鼠标悬停时的工具提示
|
||||
public override string ExtraTooltipPart()
|
||||
{
|
||||
WorldComponent_AircraftManager aircraftManager = Find.World.GetComponent<WorldComponent_AircraftManager>();
|
||||
|
||||
if (aircraftManager != null)
|
||||
{
|
||||
int available = aircraftManager.GetAvailableAircraftCount(Props.requiredAircraftType, parent.pawn.Faction);
|
||||
int onCooldown = aircraftManager.GetCooldownAircraftCount(Props.requiredAircraftType, parent.pawn.Faction);
|
||||
int total = available + onCooldown;
|
||||
|
||||
// 将冷却时间从 tick 转换为小时
|
||||
float cooldownHours = TicksToHours(Props.aircraftCooldownTicks);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine("AircraftStatusTooltip".Translate());
|
||||
sb.AppendLine("• " + "TotalAircraft".Translate(total));
|
||||
sb.AppendLine("• " + "ReadyAircraft".Translate(available));
|
||||
sb.AppendLine("• " + "CooldownAircraft".Translate(onCooldown));
|
||||
sb.AppendLine("AircraftAbilityDescription".Translate(Props.requiredAircraftType.LabelCap, Props.aircraftsPerUse, cooldownHours.ToString("F1")));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
return base.ExtraTooltipPart();
|
||||
}
|
||||
|
||||
// 将 tick 转换为小时
|
||||
private float TicksToHours(int ticks)
|
||||
{
|
||||
// RimWorld 中 1 小时 = 2500 tick
|
||||
return ticks / 2500f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Collections.Generic;
|
||||
using RimWorld.Planet;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_AircraftHangar : CompProperties
|
||||
{
|
||||
public ThingDef aircraftDef; // 对应的战机定义
|
||||
public int aircraftCount = 1; // 起飞后提供的战机数量
|
||||
public ThingDef skyfallerLeaving; // 起飞时的天空坠落者效果
|
||||
|
||||
public CompProperties_AircraftHangar()
|
||||
{
|
||||
compClass = typeof(CompAircraftHangar);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompAircraftHangar : ThingComp
|
||||
{
|
||||
public CompProperties_AircraftHangar Props => (CompProperties_AircraftHangar)props;
|
||||
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
foreach (Gizmo gizmo in base.CompGetGizmosExtra())
|
||||
{
|
||||
yield return gizmo;
|
||||
}
|
||||
|
||||
// 起飞命令
|
||||
Command_Action launchCommand = new Command_Action
|
||||
{
|
||||
defaultLabel = "LaunchAircraft".Translate(),
|
||||
defaultDesc = "LaunchAircraftDesc".Translate(),
|
||||
icon = TexCommand.Attack,
|
||||
action = LaunchAircraft
|
||||
};
|
||||
|
||||
// 检查条件:建筑完好
|
||||
if (parent.HitPoints <= 0)
|
||||
{
|
||||
launchCommand.Disable("HangarDamaged".Translate());
|
||||
}
|
||||
|
||||
yield return launchCommand;
|
||||
}
|
||||
|
||||
private void LaunchAircraft()
|
||||
{
|
||||
// 获取全局战机管理器
|
||||
WorldComponent_AircraftManager aircraftManager = Find.World.GetComponent<WorldComponent_AircraftManager>();
|
||||
|
||||
if (aircraftManager == null)
|
||||
{
|
||||
Log.Error("AircraftManagerNotFound".Translate());
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即向全局管理器注册战机
|
||||
aircraftManager.AddAircraft(Props.aircraftDef, Props.aircraftCount, parent.Faction);
|
||||
|
||||
// 显示消息
|
||||
Messages.Message("AircraftLaunched".Translate(Props.aircraftCount, Props.aircraftDef.LabelCap), MessageTypeDefOf.PositiveEvent);
|
||||
|
||||
// 创建起飞效果(仅视觉效果)
|
||||
if (Props.skyfallerLeaving != null)
|
||||
{
|
||||
CreateTakeoffEffect();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有定义 Skyfaller,直接销毁建筑
|
||||
parent.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateTakeoffEffect()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建 1 单位 Chemfuel 作为 Skyfaller 的内容物
|
||||
Thing chemfuel = ThingMaker.MakeThing(ThingDefOf.Chemfuel);
|
||||
chemfuel.stackCount = 1;
|
||||
|
||||
// 创建包含 Chemfuel 的 Skyfaller
|
||||
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(Props.skyfallerLeaving, chemfuel);
|
||||
|
||||
// 设置起飞位置(建筑当前位置)
|
||||
IntVec3 takeoffPos = parent.Position;
|
||||
|
||||
// 检查地图是否有效
|
||||
if (parent.Map == null)
|
||||
{
|
||||
Log.Error("TakeoffEffectMapNull".Translate());
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成 Skyfaller
|
||||
GenSpawn.Spawn(skyfaller, takeoffPos, parent.Map);
|
||||
|
||||
Log.Message("TakeoffSkyfallerCreated".Translate(takeoffPos));
|
||||
|
||||
// 销毁原建筑
|
||||
parent.Destroy(DestroyMode.Vanish);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error("TakeoffEffectError".Translate(ex.Message));
|
||||
// 如果Skyfaller创建失败,直接销毁建筑
|
||||
parent.Destroy(DestroyMode.Vanish);
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
// 不需要保存状态,因为建筑起飞后就销毁了
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Collections.Generic;
|
||||
using RimWorld.Planet;
|
||||
using System.Linq;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class WorldComponent_AircraftManager : WorldComponent
|
||||
{
|
||||
// 使用列表而不是嵌套字典,更容易序列化
|
||||
private List<FactionAircraftData> allFactionAircraftData = new List<FactionAircraftData>();
|
||||
private List<AircraftCooldownEvent> cooldownEvents = new List<AircraftCooldownEvent>();
|
||||
|
||||
public WorldComponent_AircraftManager(World world) : base(world) { }
|
||||
|
||||
// 派系战机数据
|
||||
private class FactionAircraftData : IExposable
|
||||
{
|
||||
public Faction faction;
|
||||
public ThingDef aircraftDef;
|
||||
public int totalCount;
|
||||
public int availableCount;
|
||||
|
||||
public void ExposeData()
|
||||
{
|
||||
Scribe_References.Look(ref faction, "faction");
|
||||
Scribe_Defs.Look(ref aircraftDef, "aircraftDef");
|
||||
Scribe_Values.Look(ref totalCount, "totalCount", 0);
|
||||
Scribe_Values.Look(ref availableCount, "availableCount", 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 冷却事件
|
||||
private class AircraftCooldownEvent : IExposable
|
||||
{
|
||||
public Faction faction;
|
||||
public ThingDef aircraftDef;
|
||||
public int endTick;
|
||||
public int aircraftCount;
|
||||
|
||||
public void ExposeData()
|
||||
{
|
||||
Scribe_References.Look(ref faction, "faction");
|
||||
Scribe_Defs.Look(ref aircraftDef, "aircraftDef");
|
||||
Scribe_Values.Look(ref endTick, "endTick", 0);
|
||||
Scribe_Values.Look(ref aircraftCount, "aircraftCount", 0);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
|
||||
// 使用简单的列表序列化
|
||||
Scribe_Collections.Look(ref allFactionAircraftData, "allFactionAircraftData", LookMode.Deep);
|
||||
Scribe_Collections.Look(ref cooldownEvents, "cooldownEvents", LookMode.Deep);
|
||||
|
||||
// 确保列表不为null
|
||||
if (allFactionAircraftData == null)
|
||||
allFactionAircraftData = new List<FactionAircraftData>();
|
||||
if (cooldownEvents == null)
|
||||
cooldownEvents = new List<AircraftCooldownEvent>();
|
||||
|
||||
// 调试日志
|
||||
if (Scribe.mode == LoadSaveMode.Saving)
|
||||
{
|
||||
Log.Message($"Saving aircraft data: {allFactionAircraftData.Count} faction entries, {cooldownEvents.Count} cooldown events");
|
||||
}
|
||||
else if (Scribe.mode == LoadSaveMode.PostLoadInit)
|
||||
{
|
||||
Log.Message($"Loaded aircraft data: {allFactionAircraftData.Count} faction entries, {cooldownEvents.Count} cooldown events");
|
||||
}
|
||||
}
|
||||
|
||||
public override void WorldComponentTick()
|
||||
{
|
||||
base.WorldComponentTick();
|
||||
|
||||
// 处理冷却事件
|
||||
int currentTick = Find.TickManager.TicksAbs;
|
||||
for (int i = cooldownEvents.Count - 1; i >= 0; i--)
|
||||
{
|
||||
AircraftCooldownEvent cooldownEvent = cooldownEvents[i];
|
||||
|
||||
if (currentTick >= cooldownEvent.endTick)
|
||||
{
|
||||
RestoreAircraftAfterCooldown(cooldownEvent);
|
||||
cooldownEvents.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取或创建派系战机数据
|
||||
private FactionAircraftData GetOrCreateFactionAircraftData(Faction faction, ThingDef aircraftDef)
|
||||
{
|
||||
var data = allFactionAircraftData.FirstOrDefault(x => x.faction == faction && x.aircraftDef == aircraftDef);
|
||||
if (data == null)
|
||||
{
|
||||
data = new FactionAircraftData
|
||||
{
|
||||
faction = faction,
|
||||
aircraftDef = aircraftDef,
|
||||
totalCount = 0,
|
||||
availableCount = 0
|
||||
};
|
||||
allFactionAircraftData.Add(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取派系战机数据(可能为null)
|
||||
private FactionAircraftData GetFactionAircraftData(Faction faction, ThingDef aircraftDef)
|
||||
{
|
||||
return allFactionAircraftData.FirstOrDefault(x => x.faction == faction && x.aircraftDef == aircraftDef);
|
||||
}
|
||||
|
||||
// 添加战机到派系
|
||||
public void AddAircraft(ThingDef aircraftDef, int count, Faction faction)
|
||||
{
|
||||
if (faction == null)
|
||||
{
|
||||
Log.Error("AddAircraftNullFaction".Translate());
|
||||
return;
|
||||
}
|
||||
|
||||
var data = GetOrCreateFactionAircraftData(faction, aircraftDef);
|
||||
data.totalCount += count;
|
||||
data.availableCount += count;
|
||||
|
||||
Log.Message($"Added {count} {aircraftDef.LabelCap} to {faction.Name}. Total: {data.totalCount}, Available: {data.availableCount}");
|
||||
}
|
||||
|
||||
// 尝试使用战机
|
||||
public bool TryUseAircraft(ThingDef aircraftDef, int count, Faction faction, int cooldownTicks)
|
||||
{
|
||||
if (!HasAvailableAircraft(aircraftDef, count, faction))
|
||||
return false;
|
||||
|
||||
var data = GetFactionAircraftData(faction, aircraftDef);
|
||||
data.availableCount -= count;
|
||||
|
||||
AircraftCooldownEvent cooldownEvent = new AircraftCooldownEvent
|
||||
{
|
||||
faction = faction,
|
||||
aircraftDef = aircraftDef,
|
||||
endTick = Find.TickManager.TicksAbs + cooldownTicks,
|
||||
aircraftCount = count
|
||||
};
|
||||
|
||||
cooldownEvents.Add(cooldownEvent);
|
||||
|
||||
Log.Message($"Used {count} {aircraftDef.LabelCap} from {faction.Name}. Available now: {data.availableCount}, Cooldown until: {cooldownEvent.endTick}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否有可用战机
|
||||
public bool HasAvailableAircraft(ThingDef aircraftDef, int count, Faction faction)
|
||||
{
|
||||
var data = GetFactionAircraftData(faction, aircraftDef);
|
||||
return data != null && data.availableCount >= count;
|
||||
}
|
||||
|
||||
// 获取可用战机数量
|
||||
public int GetAvailableAircraftCount(ThingDef aircraftDef, Faction faction)
|
||||
{
|
||||
var data = GetFactionAircraftData(faction, aircraftDef);
|
||||
return data?.availableCount ?? 0;
|
||||
}
|
||||
|
||||
// 获取总战机数量
|
||||
public int GetTotalAircraftCount(ThingDef aircraftDef, Faction faction)
|
||||
{
|
||||
var data = GetFactionAircraftData(faction, aircraftDef);
|
||||
return data?.totalCount ?? 0;
|
||||
}
|
||||
|
||||
// 冷却结束后恢复战机
|
||||
private void RestoreAircraftAfterCooldown(AircraftCooldownEvent cooldownEvent)
|
||||
{
|
||||
var data = GetFactionAircraftData(cooldownEvent.faction, cooldownEvent.aircraftDef);
|
||||
if (data != null)
|
||||
{
|
||||
data.availableCount += cooldownEvent.aircraftCount;
|
||||
|
||||
if (cooldownEvent.aircraftDef != null)
|
||||
{
|
||||
Messages.Message("AircraftCooldownEnded".Translate(cooldownEvent.aircraftDef.LabelCap), MessageTypeDefOf.PositiveEvent);
|
||||
Log.Message($"Cooldown ended for {cooldownEvent.aircraftCount} {cooldownEvent.aircraftDef.LabelCap}. Available now: {data.availableCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取冷却中的战机数量
|
||||
public int GetCooldownAircraftCount(ThingDef aircraftDef, Faction faction)
|
||||
{
|
||||
return cooldownEvents
|
||||
.Where(e => e.faction == faction && e.aircraftDef == aircraftDef)
|
||||
.Sum(e => e.aircraftCount);
|
||||
}
|
||||
|
||||
// 调试方法:显示当前状态
|
||||
public void DebugLogStatus()
|
||||
{
|
||||
Log.Message("=== Aircraft Manager Status ===");
|
||||
Log.Message($"Total faction entries: {allFactionAircraftData.Count}");
|
||||
|
||||
var factions = allFactionAircraftData.Select(x => x.faction).Distinct();
|
||||
foreach (var faction in factions)
|
||||
{
|
||||
Log.Message($"Faction: {faction?.Name ?? "Unknown"}");
|
||||
var factionData = allFactionAircraftData.Where(x => x.faction == faction);
|
||||
foreach (var data in factionData)
|
||||
{
|
||||
Log.Message($" {data.aircraftDef.LabelCap}: {data.availableCount}/{data.totalCount} available");
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message($"Active cooldown events: {cooldownEvents.Count}");
|
||||
Log.Message("===============================");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
using UnityEngine;
|
||||
using Verse.AI.Group;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// 扩展的空投仓配置属性 - 移除 dropPodDef
|
||||
public class CompProperties_FlyOverDropPods : CompProperties
|
||||
{
|
||||
public IntVec3 dropOffset = IntVec3.Zero; // 投掷位置偏移
|
||||
|
||||
// 投掷时机配置
|
||||
public float dropProgress = 0.5f; // 投掷进度 (0-1)
|
||||
public bool useCyclicDrops = false; // 是否使用循环投掷
|
||||
public float cyclicDropIntervalHours = 24f; // 循环投掷间隔(小时)
|
||||
public bool waitForExternalSignal = false; // 是否等待外部信号
|
||||
public string externalSignalTag; // 外部信号标签
|
||||
|
||||
public int dropCount = 1; // 投掷数量
|
||||
public float scatterRadius = 3f; // 散布半径
|
||||
public bool useTradeDropSpot; // 是否使用贸易空投点
|
||||
public bool allowFogged; // 是否允许雾区
|
||||
public bool dropAllInSamePod; // 是否在同一空投仓中
|
||||
public bool leaveSlag; // 是否留下残骸
|
||||
|
||||
// 内容物配置
|
||||
public List<ThingDefCountClass> thingDefs = new List<ThingDefCountClass>();
|
||||
public bool dropAllContents = false; // 是否投掷所有内容物
|
||||
|
||||
// Pawn 生成配置
|
||||
public List<PawnKindDefCountClass> pawnKinds = new List<PawnKindDefCountClass>();
|
||||
public FactionDef pawnFactionDef; // Pawn 派系定义
|
||||
public bool generatePawnsOnDrop = true; // 是否在投掷时生成 Pawn
|
||||
|
||||
// 乘客配置
|
||||
public bool joinPlayer;
|
||||
public bool makePrisoners;
|
||||
|
||||
// LordJob 配置 - 简化版本
|
||||
public bool assignAssaultLordJob = false; // 是否分配袭击殖民地的 LordJob
|
||||
public bool canKidnap = true; // 是否可以绑架
|
||||
public bool canTimeoutOrFlee = true; // 是否可以超时或逃跑
|
||||
public bool useSappers = false; // 是否使用工兵
|
||||
public bool useAvoidGridSmart = false; // 是否使用智能回避网格
|
||||
public bool canSteal = true; // 是否可以偷窃
|
||||
public bool useBreachers = false; // 是否使用破墙者
|
||||
public bool canPickUpOpportunisticWeapons = false; // 是否可以捡起机会性武器
|
||||
|
||||
// 信件通知
|
||||
public bool sendStandardLetter = true;
|
||||
public string customLetterText;
|
||||
public string customLetterLabel;
|
||||
public LetterDef customLetterDef;
|
||||
|
||||
// 派系
|
||||
public Faction faction;
|
||||
|
||||
public CompProperties_FlyOverDropPods()
|
||||
{
|
||||
this.compClass = typeof(CompFlyOverDropPods);
|
||||
}
|
||||
}
|
||||
|
||||
// PawnKind 数量类
|
||||
public class PawnKindDefCountClass : IExposable
|
||||
{
|
||||
public PawnKindDef pawnKindDef;
|
||||
public int count;
|
||||
|
||||
public PawnKindDefCountClass() { }
|
||||
|
||||
public PawnKindDefCountClass(PawnKindDef pawnKindDef, int count)
|
||||
{
|
||||
this.pawnKindDef = pawnKindDef;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public void ExposeData()
|
||||
{
|
||||
Scribe_Defs.Look(ref pawnKindDef, "pawnKindDef");
|
||||
Scribe_Values.Look(ref count, "count", 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{count}x {pawnKindDef?.label ?? "null"}";
|
||||
}
|
||||
}
|
||||
|
||||
// 空投仓投掷 Comp - 使用原版空投仓
|
||||
public class CompFlyOverDropPods : ThingComp
|
||||
{
|
||||
public CompProperties_FlyOverDropPods Props => (CompProperties_FlyOverDropPods)props;
|
||||
|
||||
// 状态变量
|
||||
private bool hasDropped = false;
|
||||
private int ticksUntilNextDrop = 0;
|
||||
private bool waitingForSignal = false;
|
||||
private List<Thing> items = new List<Thing>();
|
||||
private List<Pawn> pawns = new List<Pawn>();
|
||||
|
||||
public override void Initialize(CompProperties props)
|
||||
{
|
||||
base.Initialize(props);
|
||||
|
||||
// 预生成内容物
|
||||
if (Props.thingDefs != null)
|
||||
{
|
||||
foreach (ThingDefCountClass thingDefCount in Props.thingDefs)
|
||||
{
|
||||
Thing thing = ThingMaker.MakeThing(thingDefCount.thingDef);
|
||||
thing.stackCount = thingDefCount.count;
|
||||
items.Add(thing);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不在投掷时生成 Pawn,则预生成 Pawn
|
||||
if (!Props.generatePawnsOnDrop && Props.pawnKinds != null)
|
||||
{
|
||||
GeneratePawnsFromKinds();
|
||||
}
|
||||
|
||||
// 初始化循环投掷计时器
|
||||
if (Props.useCyclicDrops)
|
||||
{
|
||||
ticksUntilNextDrop = (int)(Props.cyclicDropIntervalHours * 2500f); // 1小时 = 2500 ticks
|
||||
Log.Message($"Cyclic drops initialized: {Props.cyclicDropIntervalHours} hours interval");
|
||||
}
|
||||
|
||||
// 初始化信号等待状态
|
||||
if (Props.waitForExternalSignal)
|
||||
{
|
||||
waitingForSignal = true;
|
||||
Log.Message($"Waiting for external signal: {Props.externalSignalTag}");
|
||||
}
|
||||
}
|
||||
|
||||
// 从 PawnKind 定义生成 Pawn
|
||||
private void GeneratePawnsFromKinds()
|
||||
{
|
||||
if (Props.pawnKinds == null) return;
|
||||
|
||||
foreach (PawnKindDefCountClass pawnKindCount in Props.pawnKinds)
|
||||
{
|
||||
for (int i = 0; i < pawnKindCount.count; i++)
|
||||
{
|
||||
Pawn pawn = GeneratePawn(pawnKindCount.pawnKindDef);
|
||||
if (pawn != null)
|
||||
{
|
||||
pawns.Add(pawn);
|
||||
Log.Message($"Generated pawn: {pawn.Label} ({pawnKindCount.pawnKindDef.defName})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成单个 Pawn
|
||||
private Pawn GeneratePawn(PawnKindDef pawnKindDef)
|
||||
{
|
||||
if (pawnKindDef == null)
|
||||
{
|
||||
Log.Error("Attempted to generate pawn with null PawnKindDef");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 确定派系
|
||||
Faction faction = DetermineFactionForPawn();
|
||||
|
||||
// 生成 Pawn
|
||||
PawnGenerationRequest request = new PawnGenerationRequest(
|
||||
pawnKindDef,
|
||||
faction,
|
||||
PawnGenerationContext.NonPlayer,
|
||||
-1,
|
||||
forceGenerateNewPawn: true,
|
||||
allowDead: false,
|
||||
allowDowned: false,
|
||||
canGeneratePawnRelations: true,
|
||||
mustBeCapableOfViolence: false,
|
||||
colonistRelationChanceFactor: 1f,
|
||||
forceAddFreeWarmLayerIfNeeded: false,
|
||||
allowGay: true,
|
||||
allowFood: true,
|
||||
allowAddictions: true,
|
||||
inhabitant: false,
|
||||
certainlyBeenInCryptosleep: false,
|
||||
forceRedressWorldPawnIfFormerColonist: false,
|
||||
worldPawnFactionDoesntMatter: false,
|
||||
biocodeWeaponChance: 0f,
|
||||
biocodeApparelChance: 0f,
|
||||
extraPawnForExtraRelationChance: null,
|
||||
relationWithExtraPawnChanceFactor: 1f,
|
||||
validatorPreGear: null,
|
||||
validatorPostGear: null,
|
||||
forcedTraits: null,
|
||||
prohibitedTraits: null,
|
||||
minChanceToRedressWorldPawn: 0f,
|
||||
fixedBiologicalAge: null,
|
||||
fixedChronologicalAge: null,
|
||||
fixedGender: null
|
||||
);
|
||||
|
||||
Pawn pawn = PawnGenerator.GeneratePawn(request);
|
||||
|
||||
// 设置 Pawn 的基本状态
|
||||
if (pawn.mindState != null)
|
||||
{
|
||||
pawn.mindState.SetupLastHumanMeatTick();
|
||||
}
|
||||
|
||||
Log.Message($"Successfully generated pawn: {pawn.LabelCap} from {pawnKindDef.defName}");
|
||||
return pawn;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"Failed to generate pawn from {pawnKindDef.defName}: {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 确定 Pawn 的派系
|
||||
private Faction DetermineFactionForPawn()
|
||||
{
|
||||
// 优先使用指定的派系定义
|
||||
if (Props.pawnFactionDef != null)
|
||||
{
|
||||
Faction faction = Find.FactionManager.FirstFactionOfDef(Props.pawnFactionDef);
|
||||
if (faction != null) return faction;
|
||||
}
|
||||
|
||||
// 使用 Comp 的派系
|
||||
if (Props.faction != null) return Props.faction;
|
||||
|
||||
// 使用默认中立派系
|
||||
return Faction.OfAncients;
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
if (parent is FlyOver flyOver)
|
||||
{
|
||||
// 检查不同的投掷模式
|
||||
if (!hasDropped && !waitingForSignal)
|
||||
{
|
||||
if (Props.useCyclicDrops)
|
||||
{
|
||||
CheckCyclicDrop(flyOver);
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckProgressDrop(flyOver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查进度投掷
|
||||
private void CheckProgressDrop(FlyOver flyOver)
|
||||
{
|
||||
if (flyOver.currentProgress >= Props.dropProgress && !hasDropped)
|
||||
{
|
||||
DropPods(flyOver);
|
||||
hasDropped = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查循环投掷
|
||||
private void CheckCyclicDrop(FlyOver flyOver)
|
||||
{
|
||||
ticksUntilNextDrop--;
|
||||
|
||||
if (ticksUntilNextDrop <= 0)
|
||||
{
|
||||
DropPods(flyOver);
|
||||
|
||||
// 重置计时器
|
||||
ticksUntilNextDrop = (int)(Props.cyclicDropIntervalHours * 2500f);
|
||||
Log.Message($"Cyclic drop completed, next drop in {Props.cyclicDropIntervalHours} hours");
|
||||
}
|
||||
}
|
||||
|
||||
// 外部信号触发投掷
|
||||
public void TriggerDropFromSignal()
|
||||
{
|
||||
if (parent is FlyOver flyOver && waitingForSignal)
|
||||
{
|
||||
Log.Message($"External signal received, triggering drop pods");
|
||||
DropPods(flyOver);
|
||||
waitingForSignal = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 接收信号的方法
|
||||
public override void ReceiveCompSignal(string signal)
|
||||
{
|
||||
base.ReceiveCompSignal(signal);
|
||||
|
||||
if (Props.waitForExternalSignal && signal == Props.externalSignalTag)
|
||||
{
|
||||
TriggerDropFromSignal();
|
||||
}
|
||||
}
|
||||
|
||||
private void DropPods(FlyOver flyOver)
|
||||
{
|
||||
Map map = flyOver.Map;
|
||||
if (map == null)
|
||||
{
|
||||
Log.Error("FlyOver DropPods: Map is null");
|
||||
return;
|
||||
}
|
||||
|
||||
IntVec3 dropCenter = GetDropCenter(flyOver);
|
||||
Log.Message($"DropPods triggered at progress {flyOver.currentProgress}, center: {dropCenter}");
|
||||
|
||||
// 如果在投掷时生成 Pawn,现在生成
|
||||
if (Props.generatePawnsOnDrop && Props.pawnKinds != null)
|
||||
{
|
||||
GeneratePawnsFromKinds();
|
||||
}
|
||||
|
||||
// 准备要投掷的物品列表
|
||||
List<Thing> thingsToDrop = new List<Thing>();
|
||||
|
||||
// 添加预生成的内容物(确保不在容器中)
|
||||
foreach (Thing item in items)
|
||||
{
|
||||
if (item.holdingOwner != null)
|
||||
{
|
||||
item.holdingOwner.Remove(item);
|
||||
}
|
||||
thingsToDrop.Add(item);
|
||||
}
|
||||
|
||||
// 添加生成的 Pawn(确保不在容器中)
|
||||
foreach (Pawn pawn in pawns)
|
||||
{
|
||||
if (pawn.holdingOwner != null)
|
||||
{
|
||||
pawn.holdingOwner.Remove(pawn);
|
||||
}
|
||||
thingsToDrop.Add(pawn);
|
||||
}
|
||||
|
||||
if (!thingsToDrop.Any())
|
||||
{
|
||||
Log.Warning("No items to drop from FlyOver drop pods");
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除已销毁的物品
|
||||
thingsToDrop.RemoveAll(x => x.Destroyed);
|
||||
|
||||
// 设置乘客派系和行为
|
||||
SetupPawnsForDrop();
|
||||
|
||||
// 执行投掷
|
||||
if (Props.dropCount > 1)
|
||||
{
|
||||
DropMultiplePods(thingsToDrop, dropCenter, map);
|
||||
}
|
||||
else
|
||||
{
|
||||
DropSinglePod(thingsToDrop, dropCenter, map);
|
||||
}
|
||||
|
||||
// 发送信件通知
|
||||
if (Props.sendStandardLetter)
|
||||
{
|
||||
SendDropLetter(thingsToDrop, dropCenter, map);
|
||||
}
|
||||
|
||||
Log.Message($"Drop pods completed: {thingsToDrop.Count} items dropped, including {pawns.Count} pawns");
|
||||
|
||||
// 清空已投掷的物品列表,避免重复投掷
|
||||
items.Clear();
|
||||
pawns.Clear();
|
||||
}
|
||||
|
||||
// 设置 Pawn 的派系和行为
|
||||
private void SetupPawnsForDrop()
|
||||
{
|
||||
foreach (Pawn pawn in pawns)
|
||||
{
|
||||
if (Props.joinPlayer)
|
||||
{
|
||||
if (pawn.Faction != Faction.OfPlayer)
|
||||
{
|
||||
pawn.SetFaction(Faction.OfPlayer);
|
||||
}
|
||||
}
|
||||
else if (Props.makePrisoners)
|
||||
{
|
||||
if (pawn.RaceProps.Humanlike && !pawn.IsPrisonerOfColony)
|
||||
{
|
||||
pawn.guest.SetGuestStatus(Faction.OfPlayer, GuestStatus.Prisoner);
|
||||
HealthUtility.TryAnesthetize(pawn);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置初始状态
|
||||
pawn.needs.SetInitialLevels();
|
||||
pawn.mindState?.SetupLastHumanMeatTick();
|
||||
}
|
||||
|
||||
// 分配 LordJob(如果启用)
|
||||
if (Props.assignAssaultLordJob && pawns.Count > 0)
|
||||
{
|
||||
AssignAssaultLordJob();
|
||||
}
|
||||
}
|
||||
|
||||
// 分配袭击殖民地的 LordJob
|
||||
private void AssignAssaultLordJob()
|
||||
{
|
||||
// 按派系分组 Pawn
|
||||
var pawnsByFaction = new Dictionary<Faction, List<Pawn>>();
|
||||
|
||||
foreach (Pawn pawn in pawns)
|
||||
{
|
||||
// 跳过玩家派系和囚犯
|
||||
if (pawn.Faction == Faction.OfPlayer || pawn.IsPrisonerOfColony)
|
||||
continue;
|
||||
|
||||
// 跳过无效派系
|
||||
if (pawn.Faction == null)
|
||||
continue;
|
||||
|
||||
if (!pawnsByFaction.ContainsKey(pawn.Faction))
|
||||
{
|
||||
pawnsByFaction[pawn.Faction] = new List<Pawn>();
|
||||
}
|
||||
pawnsByFaction[pawn.Faction].Add(pawn);
|
||||
}
|
||||
|
||||
// 为每个派系创建 LordJob
|
||||
foreach (var factionGroup in pawnsByFaction)
|
||||
{
|
||||
Faction faction = factionGroup.Key;
|
||||
List<Pawn> factionPawns = factionGroup.Value;
|
||||
|
||||
if (factionPawns.Count == 0)
|
||||
continue;
|
||||
|
||||
// 创建 LordJob_AssaultColony
|
||||
LordJob_AssaultColony lordJob = new LordJob_AssaultColony(
|
||||
faction,
|
||||
Props.canKidnap,
|
||||
Props.canTimeoutOrFlee,
|
||||
Props.useSappers,
|
||||
Props.useAvoidGridSmart,
|
||||
Props.canSteal,
|
||||
Props.useBreachers,
|
||||
Props.canPickUpOpportunisticWeapons
|
||||
);
|
||||
|
||||
// 创建 Lord
|
||||
Lord lord = LordMaker.MakeNewLord(faction, lordJob, Find.CurrentMap, factionPawns);
|
||||
|
||||
Log.Message($"Assigned assault lord job to {factionPawns.Count} pawns of faction {faction.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DropSinglePod(List<Thing> thingsToDrop, IntVec3 dropCenter, Map map)
|
||||
{
|
||||
// 使用原版空投仓系统
|
||||
if (Props.dropAllInSamePod)
|
||||
{
|
||||
// 所有物品在一个空投仓中
|
||||
DropPodUtility.DropThingGroupsNear(
|
||||
dropCenter,
|
||||
map,
|
||||
new List<List<Thing>> { thingsToDrop },
|
||||
openDelay: 110,
|
||||
instaDrop: false,
|
||||
leaveSlag: Props.leaveSlag,
|
||||
canRoofPunch: !Props.useTradeDropSpot,
|
||||
forbid: false,
|
||||
allowFogged: Props.allowFogged,
|
||||
canTransfer: false,
|
||||
faction: Props.faction
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 每个物品单独空投仓
|
||||
DropPodUtility.DropThingsNear(
|
||||
dropCenter,
|
||||
map,
|
||||
thingsToDrop,
|
||||
openDelay: 110,
|
||||
canInstaDropDuringInit: false,
|
||||
leaveSlag: Props.leaveSlag,
|
||||
canRoofPunch: !Props.useTradeDropSpot,
|
||||
forbid: false,
|
||||
allowFogged: Props.allowFogged,
|
||||
faction: Props.faction
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void DropMultiplePods(List<Thing> thingsToDrop, IntVec3 dropCenter, Map map)
|
||||
{
|
||||
List<List<Thing>> podGroups = new List<List<Thing>>();
|
||||
|
||||
// 首先,确保所有物品都不在任何容器中
|
||||
foreach (Thing thing in thingsToDrop)
|
||||
{
|
||||
if (thing.holdingOwner != null)
|
||||
{
|
||||
thing.holdingOwner.Remove(thing);
|
||||
}
|
||||
}
|
||||
|
||||
if (Props.dropAllInSamePod)
|
||||
{
|
||||
// 所有物品在一个空投仓中,但生成多个相同的空投仓
|
||||
for (int i = 0; i < Props.dropCount; i++)
|
||||
{
|
||||
List<Thing> podItems = CreatePodItemsCopy(thingsToDrop);
|
||||
podGroups.Add(podItems);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 将原始物品分配到多个空投仓中
|
||||
List<Thing> remainingItems = new List<Thing>(thingsToDrop);
|
||||
|
||||
for (int i = 0; i < Props.dropCount; i++)
|
||||
{
|
||||
List<Thing> podItems = new List<Thing>();
|
||||
int itemsPerPod = Mathf.CeilToInt((float)remainingItems.Count / (Props.dropCount - i));
|
||||
|
||||
for (int j = 0; j < itemsPerPod && remainingItems.Count > 0; j++)
|
||||
{
|
||||
podItems.Add(remainingItems[0]);
|
||||
remainingItems.RemoveAt(0);
|
||||
}
|
||||
|
||||
podGroups.Add(podItems);
|
||||
}
|
||||
}
|
||||
|
||||
// 投掷多个空投仓组
|
||||
foreach (List<Thing> podGroup in podGroups)
|
||||
{
|
||||
if (podGroup.Count == 0) continue;
|
||||
|
||||
IntVec3 scatterPos = GetScatteredDropPos(dropCenter, map);
|
||||
DropPodUtility.DropThingGroupsNear(
|
||||
scatterPos,
|
||||
map,
|
||||
new List<List<Thing>> { podGroup },
|
||||
openDelay: 110,
|
||||
instaDrop: false,
|
||||
leaveSlag: Props.leaveSlag,
|
||||
canRoofPunch: !Props.useTradeDropSpot,
|
||||
forbid: false,
|
||||
allowFogged: Props.allowFogged,
|
||||
canTransfer: false,
|
||||
faction: Props.faction
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建物品的深拷贝
|
||||
private List<Thing> CreatePodItemsCopy(List<Thing> originalItems)
|
||||
{
|
||||
List<Thing> copies = new List<Thing>();
|
||||
|
||||
foreach (Thing original in originalItems)
|
||||
{
|
||||
if (original is Pawn originalPawn)
|
||||
{
|
||||
// 对于 Pawn,重新生成
|
||||
Pawn newPawn = GeneratePawn(originalPawn.kindDef);
|
||||
if (newPawn != null)
|
||||
{
|
||||
copies.Add(newPawn);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 对于物品,创建副本
|
||||
Thing copy = ThingMaker.MakeThing(original.def, original.Stuff);
|
||||
copy.stackCount = original.stackCount;
|
||||
|
||||
// 复制其他重要属性
|
||||
if (original.def.useHitPoints)
|
||||
{
|
||||
copy.HitPoints = original.HitPoints;
|
||||
}
|
||||
|
||||
copies.Add(copy);
|
||||
}
|
||||
}
|
||||
|
||||
return copies;
|
||||
}
|
||||
|
||||
private IntVec3 GetDropCenter(FlyOver flyOver)
|
||||
{
|
||||
// 计算投掷中心位置(基于当前飞行位置 + 偏移)
|
||||
Vector3 currentPos = Vector3.Lerp(
|
||||
flyOver.startPosition.ToVector3(),
|
||||
flyOver.endPosition.ToVector3(),
|
||||
flyOver.currentProgress
|
||||
);
|
||||
|
||||
IntVec3 dropCenter = currentPos.ToIntVec3() + Props.dropOffset;
|
||||
|
||||
// 如果使用贸易空投点,找到贸易空投点
|
||||
if (Props.useTradeDropSpot)
|
||||
{
|
||||
dropCenter = DropCellFinder.TradeDropSpot(flyOver.Map);
|
||||
}
|
||||
|
||||
return dropCenter;
|
||||
}
|
||||
|
||||
private IntVec3 GetScatteredDropPos(IntVec3 center, Map map)
|
||||
{
|
||||
if (Props.scatterRadius <= 0)
|
||||
return center;
|
||||
|
||||
// 在散布半径内找到有效位置
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
IntVec3 scatterPos = center + new IntVec3(
|
||||
Rand.RangeInclusive(-(int)Props.scatterRadius, (int)Props.scatterRadius),
|
||||
0,
|
||||
Rand.RangeInclusive(-(int)Props.scatterRadius, (int)Props.scatterRadius)
|
||||
);
|
||||
|
||||
if (scatterPos.InBounds(map) &&
|
||||
scatterPos.Standable(map) &&
|
||||
!scatterPos.Roofed(map) &&
|
||||
(Props.allowFogged || !scatterPos.Fogged(map)))
|
||||
{
|
||||
return scatterPos;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找不到有效位置,返回随机空投点
|
||||
return DropCellFinder.RandomDropSpot(map);
|
||||
}
|
||||
|
||||
private void SendDropLetter(List<Thing> thingsToDrop, IntVec3 dropCenter, Map map)
|
||||
{
|
||||
TaggedString text = null;
|
||||
TaggedString label = null;
|
||||
|
||||
// 生成信件内容(模仿原版逻辑)
|
||||
if (Props.joinPlayer && pawns.Count == 1 && pawns[0].RaceProps.Humanlike)
|
||||
{
|
||||
text = "LetterRefugeeJoins".Translate(pawns[0].Named("PAWN"));
|
||||
label = "LetterLabelRefugeeJoins".Translate(pawns[0].Named("PAWN"));
|
||||
PawnRelationUtility.TryAppendRelationsWithColonistsInfo(ref text, ref label, pawns[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
text = "LetterQuestDropPodsArrived".Translate(GenLabel.ThingsLabel(thingsToDrop));
|
||||
label = "LetterLabelQuestDropPodsArrived".Translate();
|
||||
PawnRelationUtility.Notify_PawnsSeenByPlayer_Letter(
|
||||
pawns,
|
||||
ref label,
|
||||
ref text,
|
||||
"LetterRelatedPawnsNeutralGroup".Translate(Faction.OfPlayer.def.pawnsPlural),
|
||||
informEvenIfSeenBefore: true
|
||||
);
|
||||
}
|
||||
|
||||
// 应用自定义文本
|
||||
label = (Props.customLetterLabel.NullOrEmpty() ? label : Props.customLetterLabel.Formatted(label.Named("BASELABEL")));
|
||||
text = (Props.customLetterText.NullOrEmpty() ? text : Props.customLetterText.Formatted(text.Named("BASETEXT")));
|
||||
|
||||
// 发送信件
|
||||
Find.LetterStack.ReceiveLetter(
|
||||
label,
|
||||
text,
|
||||
Props.customLetterDef ?? LetterDefOf.PositiveEvent,
|
||||
new TargetInfo(dropCenter, map)
|
||||
);
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref hasDropped, "hasDropped", false);
|
||||
Scribe_Values.Look(ref ticksUntilNextDrop, "ticksUntilNextDrop", 0);
|
||||
Scribe_Values.Look(ref waitingForSignal, "waitingForSignal", false);
|
||||
Scribe_Collections.Look(ref items, "items", LookMode.Deep);
|
||||
Scribe_Collections.Look(ref pawns, "pawns", LookMode.Reference);
|
||||
}
|
||||
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
if (DebugSettings.ShowDevGizmos && parent is FlyOver)
|
||||
{
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "Dev: Trigger Drop Pods",
|
||||
action = () => DropPods(parent as FlyOver)
|
||||
};
|
||||
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "Dev: Generate Pawns Now",
|
||||
action = () =>
|
||||
{
|
||||
GeneratePawnsFromKinds();
|
||||
Messages.Message($"Generated {pawns.Count} pawns", MessageTypeDefOf.NeutralEvent);
|
||||
}
|
||||
};
|
||||
|
||||
if (Props.waitForExternalSignal)
|
||||
{
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "Dev: Send External Signal",
|
||||
action = () => TriggerDropFromSignal()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 公共方法:供其他 Comps 调用以触发投掷
|
||||
public void TriggerDropPods()
|
||||
{
|
||||
if (parent is FlyOver flyOver)
|
||||
{
|
||||
DropPods(flyOver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompFlyOverEscort : ThingComp
|
||||
{
|
||||
public CompProperties_FlyOverEscort Props => (CompProperties_FlyOverEscort)props;
|
||||
|
||||
// 状态变量
|
||||
private float ticksUntilNextSpawn = 0f;
|
||||
private List<FlyOver> activeEscorts = new List<FlyOver>();
|
||||
private bool hasInitialized = false;
|
||||
|
||||
// 存储每个伴飞的缩放和遮罩数据
|
||||
private Dictionary<FlyOver, EscortVisualData> escortVisualData = new Dictionary<FlyOver, EscortVisualData>();
|
||||
|
||||
public override void Initialize(CompProperties props)
|
||||
{
|
||||
base.Initialize(props);
|
||||
|
||||
if (Props.spawnOnStart)
|
||||
{
|
||||
ticksUntilNextSpawn = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
ticksUntilNextSpawn = Props.spawnIntervalTicks;
|
||||
}
|
||||
|
||||
Log.Message($"FlyOver Escort initialized: {Props.spawnIntervalTicks} ticks interval, max {Props.maxEscorts} escorts");
|
||||
Log.Message($"Safe distances - From Main: {Props.minSafeDistanceFromMain}, Between Escorts: {Props.minSafeDistanceBetweenEscorts}");
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
if (parent is not FlyOver mainFlyOver || !mainFlyOver.Spawned || mainFlyOver.Map == null)
|
||||
return;
|
||||
|
||||
// 初始化检查
|
||||
if (!hasInitialized && mainFlyOver.hasStarted)
|
||||
{
|
||||
hasInitialized = true;
|
||||
Log.Message($"FlyOver Escort: Main FlyOver started at {mainFlyOver.startPosition}");
|
||||
}
|
||||
|
||||
// 清理已销毁的伴飞
|
||||
CleanupDestroyedEscorts();
|
||||
|
||||
// 检查是否应该生成新伴飞
|
||||
if (ShouldSpawnEscort(mainFlyOver))
|
||||
{
|
||||
ticksUntilNextSpawn -= 1f;
|
||||
|
||||
if (ticksUntilNextSpawn <= 0f)
|
||||
{
|
||||
SpawnEscorts(mainFlyOver);
|
||||
ticksUntilNextSpawn = Props.spawnIntervalTicks;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新现有伴飞的位置(如果需要)
|
||||
UpdateEscortPositions(mainFlyOver);
|
||||
}
|
||||
|
||||
private void CleanupDestroyedEscorts()
|
||||
{
|
||||
// 清理已销毁的伴飞
|
||||
for (int i = activeEscorts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (activeEscorts[i] == null || activeEscorts[i].Destroyed || !activeEscorts[i].Spawned)
|
||||
{
|
||||
FlyOver removedEscort = activeEscorts[i];
|
||||
activeEscorts.RemoveAt(i);
|
||||
escortVisualData.Remove(removedEscort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldSpawnEscort(FlyOver mainFlyOver)
|
||||
{
|
||||
if (!mainFlyOver.hasStarted || mainFlyOver.hasCompleted)
|
||||
return false;
|
||||
|
||||
if (!Props.continuousSpawning && activeEscorts.Count >= Props.spawnCount)
|
||||
return false;
|
||||
|
||||
if (activeEscorts.Count >= Props.maxEscorts)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SpawnEscorts(FlyOver mainFlyOver)
|
||||
{
|
||||
int escortsToSpawn = Mathf.Min(Props.spawnCount, Props.maxEscorts - activeEscorts.Count);
|
||||
int successfulSpawns = 0;
|
||||
int maxAttempts = escortsToSpawn * 5; // 最多尝试5倍的数量
|
||||
|
||||
for (int attempt = 0; attempt < maxAttempts && successfulSpawns < escortsToSpawn; attempt++)
|
||||
{
|
||||
// 先生成视觉数据
|
||||
EscortVisualData visualData = GenerateEscortVisualData();
|
||||
|
||||
FlyOver escort = CreateEscort(mainFlyOver, visualData);
|
||||
if (escort != null)
|
||||
{
|
||||
// 检查安全距离
|
||||
if (IsSafeDistance(escort, mainFlyOver))
|
||||
{
|
||||
activeEscorts.Add(escort);
|
||||
escortVisualData[escort] = visualData;
|
||||
successfulSpawns++;
|
||||
|
||||
Log.Message($"Spawned escort #{successfulSpawns} for FlyOver at {mainFlyOver.DrawPos}, scale: {visualData.scale:F2}, maskAlpha: {visualData.heightMaskAlpha:F2}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 不安全,销毁这个伴飞
|
||||
escort.Destroy();
|
||||
Log.Message($"Escort spawn attempt {attempt + 1}: Position too close to existing escort, trying again");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已经生成足够数量,提前退出
|
||||
if (successfulSpawns >= escortsToSpawn)
|
||||
break;
|
||||
}
|
||||
|
||||
if (successfulSpawns < escortsToSpawn)
|
||||
{
|
||||
Log.Message($"Spawned {successfulSpawns}/{escortsToSpawn} escorts (some positions were too close to existing escorts)");
|
||||
}
|
||||
}
|
||||
|
||||
// 修改:分别检查与主飞行物和伴飞物的安全距离
|
||||
private bool IsSafeDistance(FlyOver newEscort, FlyOver mainFlyOver)
|
||||
{
|
||||
Vector3 newPos = newEscort.DrawPos;
|
||||
|
||||
// 检查与主FlyOver的距离
|
||||
if (Props.minSafeDistanceFromMain > 0)
|
||||
{
|
||||
float distToMain = Vector3.Distance(newPos, mainFlyOver.DrawPos);
|
||||
if (distToMain < Props.minSafeDistanceFromMain)
|
||||
{
|
||||
Log.Message($"Escort too close to main FlyOver: {distToMain:F1} < {Props.minSafeDistanceFromMain}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查与其他伴飞的距离
|
||||
if (Props.minSafeDistanceBetweenEscorts > 0)
|
||||
{
|
||||
foreach (FlyOver existingEscort in activeEscorts)
|
||||
{
|
||||
if (existingEscort == null || existingEscort.Destroyed)
|
||||
continue;
|
||||
|
||||
float distToEscort = Vector3.Distance(newPos, existingEscort.DrawPos);
|
||||
if (distToEscort < Props.minSafeDistanceBetweenEscorts)
|
||||
{
|
||||
Log.Message($"Escort too close to existing escort: {distToEscort:F1} < {Props.minSafeDistanceBetweenEscorts}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private EscortVisualData GenerateEscortVisualData()
|
||||
{
|
||||
EscortVisualData data = new EscortVisualData();
|
||||
|
||||
// 随机生成缩放比例
|
||||
data.scale = Props.escortScaleRange.RandomInRange;
|
||||
|
||||
// 根据缩放计算遮罩透明度(小的飞得更高,更透明)
|
||||
float scaleFactor = Mathf.InverseLerp(Props.escortScaleRange.min, Props.escortScaleRange.max, data.scale);
|
||||
data.heightMaskAlpha = Mathf.Lerp(Props.heightMaskAlphaRange.max, Props.heightMaskAlphaRange.min, scaleFactor);
|
||||
|
||||
// 计算遮罩缩放
|
||||
data.heightMaskScale = data.scale * Props.heightMaskScaleMultiplier;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private FlyOver CreateEscort(FlyOver mainFlyOver, EscortVisualData visualData)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 选择伴飞定义
|
||||
ThingDef escortDef = SelectEscortDef();
|
||||
if (escortDef == null)
|
||||
{
|
||||
Log.Error("FlyOver Escort: No valid escort def found");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算伴飞的起点和终点
|
||||
IntVec3 escortStart = CalculateEscortStart(mainFlyOver);
|
||||
IntVec3 escortEnd = CalculateEscortEnd(mainFlyOver, escortStart);
|
||||
|
||||
if (!escortStart.InBounds(mainFlyOver.Map) || !escortEnd.InBounds(mainFlyOver.Map))
|
||||
{
|
||||
Log.Warning("FlyOver Escort: Escort start or end position out of bounds");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算伴飞参数
|
||||
float escortSpeed = mainFlyOver.flightSpeed * Props.escortSpeedMultiplier;
|
||||
float escortAltitude = mainFlyOver.altitude + Props.escortAltitudeOffset;
|
||||
|
||||
// 创建伴飞
|
||||
FlyOver escort = FlyOver.MakeFlyOver(
|
||||
escortDef,
|
||||
escortStart,
|
||||
escortEnd,
|
||||
mainFlyOver.Map,
|
||||
escortSpeed,
|
||||
escortAltitude,
|
||||
null, // 没有内容物
|
||||
mainFlyOver.fadeInDuration
|
||||
);
|
||||
|
||||
// 设置伴飞属性 - 现在传入 visualData
|
||||
SetupEscortProperties(escort, mainFlyOver, visualData);
|
||||
|
||||
Log.Message($"Created escort: {escortStart} -> {escortEnd}, speed: {escortSpeed}, altitude: {escortAltitude}");
|
||||
|
||||
return escort;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"Error creating FlyOver escort: {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ThingDef SelectEscortDef()
|
||||
{
|
||||
if (Props.escortFlyOverDefs != null && Props.escortFlyOverDefs.Count > 0)
|
||||
{
|
||||
return Props.escortFlyOverDefs.RandomElement();
|
||||
}
|
||||
|
||||
return Props.escortFlyOverDef;
|
||||
}
|
||||
|
||||
private IntVec3 CalculateEscortStart(FlyOver mainFlyOver)
|
||||
{
|
||||
Vector3 mainDirection = mainFlyOver.MovementDirection;
|
||||
Vector3 mainPosition = mainFlyOver.DrawPos;
|
||||
|
||||
// 计算横向偏移方向(垂直于飞行方向)
|
||||
Vector3 lateralDirection = GetLateralOffsetDirection(mainDirection);
|
||||
|
||||
// 计算偏移量
|
||||
float lateralOffset = Props.useRandomOffset ?
|
||||
Rand.Range(-Props.lateralOffset, Props.lateralOffset) :
|
||||
Props.lateralOffset;
|
||||
|
||||
float spawnDistance = Props.useRandomOffset ?
|
||||
Rand.Range(Props.spawnDistance * 0.5f, Props.spawnDistance * 1.5f) :
|
||||
Props.spawnDistance;
|
||||
|
||||
// 计算起点位置(从主FlyOver后方偏移)
|
||||
Vector3 offset = (-mainDirection * spawnDistance) + (lateralDirection * lateralOffset);
|
||||
Vector3 escortStartPos = mainPosition + offset;
|
||||
|
||||
// 确保位置在地图边界内
|
||||
IntVec3 escortStart = escortStartPos.ToIntVec3();
|
||||
if (!escortStart.InBounds(mainFlyOver.Map))
|
||||
{
|
||||
// 如果超出边界,调整到边界内
|
||||
escortStart = ClampToMap(escortStart, mainFlyOver.Map);
|
||||
}
|
||||
|
||||
return escortStart;
|
||||
}
|
||||
|
||||
private IntVec3 CalculateEscortEnd(FlyOver mainFlyOver, IntVec3 escortStart)
|
||||
{
|
||||
Vector3 mainDirection = mainFlyOver.MovementDirection;
|
||||
Vector3 mainEndPos = mainFlyOver.endPosition.ToVector3();
|
||||
|
||||
// 如果镜像移动,使用相反方向
|
||||
if (Props.mirrorMovement)
|
||||
{
|
||||
mainDirection = -mainDirection;
|
||||
}
|
||||
|
||||
// 计算从起点沿飞行方向延伸的终点
|
||||
float flightDistance = mainFlyOver.startPosition.DistanceTo(mainFlyOver.endPosition);
|
||||
Vector3 escortEndPos = escortStart.ToVector3() + (mainDirection * flightDistance);
|
||||
|
||||
// 确保终点在地图边界内
|
||||
IntVec3 escortEnd = escortEndPos.ToIntVec3();
|
||||
if (!escortEnd.InBounds(mainFlyOver.Map))
|
||||
{
|
||||
escortEnd = ClampToMap(escortEnd, mainFlyOver.Map);
|
||||
}
|
||||
|
||||
return escortEnd;
|
||||
}
|
||||
|
||||
private Vector3 GetLateralOffsetDirection(Vector3 mainDirection)
|
||||
{
|
||||
// 获取垂直于飞行方向的向量(随机选择左侧或右侧)
|
||||
Vector3 lateral = new Vector3(-mainDirection.z, 0f, mainDirection.x);
|
||||
|
||||
// 随机选择方向
|
||||
if (Rand.Value > 0.5f)
|
||||
{
|
||||
lateral = -lateral;
|
||||
}
|
||||
|
||||
return lateral.normalized;
|
||||
}
|
||||
|
||||
private IntVec3 ClampToMap(IntVec3 position, Map map)
|
||||
{
|
||||
CellRect mapRect = CellRect.WholeMap(map);
|
||||
return new IntVec3(
|
||||
Mathf.Clamp(position.x, mapRect.minX, mapRect.maxX),
|
||||
0,
|
||||
Mathf.Clamp(position.z, mapRect.minZ, mapRect.maxZ)
|
||||
);
|
||||
}
|
||||
|
||||
private void SetupEscortProperties(FlyOver escort, FlyOver mainFlyOver, EscortVisualData visualData)
|
||||
{
|
||||
// 设置伴飞缩放 - 现在直接从参数获取
|
||||
escort.escortScale = visualData.scale;
|
||||
escort.isEscort = true;
|
||||
|
||||
// 禁用阴影(如果需要)
|
||||
if (!mainFlyOver.createShadow)
|
||||
{
|
||||
escort.createShadow = false;
|
||||
}
|
||||
|
||||
// 禁用音效(如果需要)
|
||||
if (!mainFlyOver.playFlyOverSound)
|
||||
{
|
||||
escort.playFlyOverSound = false;
|
||||
}
|
||||
|
||||
Log.Message($"Set escort properties: scale={visualData.scale:F2}, isEscort={escort.isEscort}");
|
||||
}
|
||||
|
||||
private void UpdateEscortPositions(FlyOver mainFlyOver)
|
||||
{
|
||||
// 如果需要实时更新伴飞位置,可以在这里实现
|
||||
// 目前伴飞会按照自己的路径飞行
|
||||
}
|
||||
|
||||
// 新增:在绘制时调用
|
||||
public override void PostDraw()
|
||||
{
|
||||
base.PostDraw();
|
||||
DrawEscortHeightMasks();
|
||||
}
|
||||
|
||||
// 新增:绘制伴飞的高度遮罩
|
||||
public void DrawEscortHeightMasks()
|
||||
{
|
||||
if (!Props.useHeightMask || escortVisualData.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var kvp in escortVisualData)
|
||||
{
|
||||
FlyOver escort = kvp.Key;
|
||||
EscortVisualData visualData = kvp.Value;
|
||||
|
||||
if (escort == null || escort.Destroyed || !escort.Spawned)
|
||||
continue;
|
||||
|
||||
DrawHeightMaskForEscort(escort, visualData);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawHeightMaskForEscort(FlyOver escort, EscortVisualData visualData)
|
||||
{
|
||||
if (visualData.heightMaskAlpha <= 0.01f)
|
||||
return;
|
||||
|
||||
// 获取伴飞的绘制位置
|
||||
Vector3 drawPos = escort.DrawPos;
|
||||
drawPos.y = AltitudeLayer.MetaOverlays.AltitudeFor() + 0.01f; // 稍微高于伴飞本身
|
||||
|
||||
// 计算遮罩矩阵
|
||||
Matrix4x4 matrix = Matrix4x4.TRS(
|
||||
drawPos,
|
||||
escort.ExactRotation,
|
||||
new Vector3(visualData.heightMaskScale, 1f, visualData.heightMaskScale)
|
||||
);
|
||||
|
||||
// 设置遮罩材质属性
|
||||
Material heightMaskMat = GetHeightMaskMaterial();
|
||||
if (heightMaskMat != null)
|
||||
{
|
||||
// 计算最终颜色和透明度
|
||||
Color finalColor = Props.heightMaskColor;
|
||||
finalColor.a *= visualData.heightMaskAlpha * escort.OverallAlpha;
|
||||
|
||||
var propertyBlock = new MaterialPropertyBlock();
|
||||
propertyBlock.SetColor(ShaderPropertyIDs.Color, finalColor);
|
||||
|
||||
// 绘制遮罩
|
||||
Graphics.DrawMesh(
|
||||
MeshPool.plane10,
|
||||
matrix,
|
||||
heightMaskMat,
|
||||
0, // layer
|
||||
null, // camera
|
||||
0, // submeshIndex
|
||||
propertyBlock
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Material heightMaskMaterial;
|
||||
private Material GetHeightMaskMaterial()
|
||||
{
|
||||
if (heightMaskMaterial == null)
|
||||
{
|
||||
// 创建一个简单的圆形遮罩材质
|
||||
heightMaskMaterial = MaterialPool.MatFrom("UI/Overlays/SoftShadowCircle", ShaderDatabase.Transparent);
|
||||
}
|
||||
return heightMaskMaterial;
|
||||
}
|
||||
|
||||
public override void PostDestroy(DestroyMode mode, Map previousMap)
|
||||
{
|
||||
base.PostDestroy(mode, previousMap);
|
||||
|
||||
// 销毁所有伴飞
|
||||
if (Props.destroyWithParent)
|
||||
{
|
||||
foreach (FlyOver escort in activeEscorts)
|
||||
{
|
||||
if (escort != null && escort.Spawned)
|
||||
{
|
||||
escort.Destroy();
|
||||
}
|
||||
}
|
||||
activeEscorts.Clear();
|
||||
escortVisualData.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref ticksUntilNextSpawn, "ticksUntilNextSpawn", 0f);
|
||||
Scribe_Collections.Look(ref activeEscorts, "activeEscorts", LookMode.Reference);
|
||||
Scribe_Values.Look(ref hasInitialized, "hasInitialized", false);
|
||||
|
||||
// 保存视觉数据(如果需要)
|
||||
if (Scribe.mode == LoadSaveMode.Saving)
|
||||
{
|
||||
List<FlyOver> keys = new List<FlyOver>(escortVisualData.Keys);
|
||||
List<EscortVisualData> values = new List<EscortVisualData>(escortVisualData.Values);
|
||||
Scribe_Collections.Look(ref keys, "escortKeys", LookMode.Reference);
|
||||
Scribe_Collections.Look(ref values, "escortValues", LookMode.Deep);
|
||||
}
|
||||
else if (Scribe.mode == LoadSaveMode.LoadingVars)
|
||||
{
|
||||
List<FlyOver> keys = new List<FlyOver>();
|
||||
List<EscortVisualData> values = new List<EscortVisualData>();
|
||||
Scribe_Collections.Look(ref keys, "escortKeys", LookMode.Reference);
|
||||
Scribe_Collections.Look(ref values, "escortValues", LookMode.Deep);
|
||||
|
||||
if (keys != null && values != null && keys.Count == values.Count)
|
||||
{
|
||||
escortVisualData.Clear();
|
||||
for (int i = 0; i < keys.Count; i++)
|
||||
{
|
||||
escortVisualData[keys[i]] = values[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 公共方法:强制生成伴飞
|
||||
public void SpawnEscortNow()
|
||||
{
|
||||
if (parent is FlyOver flyOver)
|
||||
{
|
||||
SpawnEscorts(flyOver);
|
||||
}
|
||||
}
|
||||
|
||||
// 公共方法:获取活跃伴飞数量
|
||||
public int GetActiveEscortCount()
|
||||
{
|
||||
return activeEscorts.Count;
|
||||
}
|
||||
|
||||
// 新增:获取伴飞的视觉数据
|
||||
public EscortVisualData GetEscortVisualData(FlyOver escort)
|
||||
{
|
||||
if (escortVisualData.TryGetValue(escort, out var data))
|
||||
{
|
||||
return data;
|
||||
}
|
||||
return new EscortVisualData { scale = 1f, heightMaskAlpha = 1f, heightMaskScale = 1f };
|
||||
}
|
||||
}
|
||||
|
||||
// 伴飞视觉数据类
|
||||
public class EscortVisualData : IExposable
|
||||
{
|
||||
public float scale = 1f;
|
||||
public float heightMaskAlpha = 1f;
|
||||
public float heightMaskScale = 1f;
|
||||
|
||||
public void ExposeData()
|
||||
{
|
||||
Scribe_Values.Look(ref scale, "scale", 1f);
|
||||
Scribe_Values.Look(ref heightMaskAlpha, "heightMaskAlpha", 1f);
|
||||
Scribe_Values.Look(ref heightMaskScale, "heightMaskScale", 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_FlyOverEscort : CompProperties
|
||||
{
|
||||
// 伴飞配置
|
||||
public ThingDef escortFlyOverDef; // 伴飞FlyOver定义
|
||||
public List<ThingDef> escortFlyOverDefs; // 多个伴飞定义(随机选择)
|
||||
|
||||
// 生成配置
|
||||
public float spawnIntervalTicks = 600f; // 生成间隔(tick)
|
||||
public int maxEscorts = 3; // 最大伴飞数量
|
||||
public int spawnCount = 1; // 每次生成的伴飞数量
|
||||
|
||||
// 位置配置
|
||||
public float spawnDistance = 10f; // 生成距离(从主FlyOver)
|
||||
public float lateralOffset = 5f; // 横向偏移量
|
||||
public float verticalOffset = 2f; // 垂直偏移量(高度差)
|
||||
public bool useRandomOffset = true; // 是否使用随机偏移
|
||||
|
||||
// 修改:独立的安全距离配置
|
||||
public float minSafeDistanceFromMain = 8f; // 与主飞行物的最小安全距离(单元格)
|
||||
public float minSafeDistanceBetweenEscorts = 3f; // 伴飞物之间的最小安全距离(单元格)
|
||||
|
||||
// 飞行配置
|
||||
public float escortSpeedMultiplier = 1f; // 速度乘数(相对于主FlyOver)
|
||||
public float escortAltitudeOffset = 0f; // 高度偏移
|
||||
public bool mirrorMovement = false; // 是否镜像移动(相反方向)
|
||||
|
||||
// 行为配置
|
||||
public bool spawnOnStart = true; // 开始时立即生成
|
||||
public bool continuousSpawning = true; // 是否持续生成
|
||||
public bool destroyWithParent = true; // 是否随父级销毁
|
||||
|
||||
// 外观配置
|
||||
public float escortScale = 1f; // 缩放比例(向后兼容)
|
||||
public FloatRange escortScaleRange = new FloatRange(0.5f, 1.5f); // 缩放比例区间
|
||||
public bool useParentRotation = true; // 使用父级旋转
|
||||
|
||||
// 新增:高度遮罩配置
|
||||
public bool useHeightMask = true; // 是否使用高度遮罩
|
||||
public FloatRange heightMaskAlphaRange = new FloatRange(0.3f, 0.8f); // 遮罩透明度区间
|
||||
public Color heightMaskColor = new Color(0.8f, 0.9f, 1.0f, 1f); // 遮罩颜色(淡蓝色)
|
||||
public float heightMaskScaleMultiplier = 1.2f; // 遮罩缩放倍数
|
||||
|
||||
public CompProperties_FlyOverEscort()
|
||||
{
|
||||
compClass = typeof(CompFlyOverEscort);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompGroundStrafing : ThingComp
|
||||
{
|
||||
public CompProperties_GroundStrafing Props => (CompProperties_GroundStrafing)props;
|
||||
|
||||
// 简化的扫射状态
|
||||
private List<IntVec3> confirmedTargetCells = new List<IntVec3>();
|
||||
private HashSet<IntVec3> firedCells = new HashSet<IntVec3>();
|
||||
|
||||
// 横向偏移状态(左右)
|
||||
private float currentLateralOffsetAngle = 0f;
|
||||
private int shotsFired = 0;
|
||||
private Vector3 lastProjectileDirection = Vector3.zero;
|
||||
|
||||
// 新增:纵向偏移状态(前后)
|
||||
private float currentLongitudinalOffset = 0f; // 当前纵向偏移距离
|
||||
private bool isForwardPhase = true; // 是否处于向前偏移阶段
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
base.PostSpawnSetup(respawningAfterLoad);
|
||||
|
||||
// 初始化偏移
|
||||
if (!respawningAfterLoad)
|
||||
{
|
||||
currentLateralOffsetAngle = Props.lateralInitialOffsetAngle;
|
||||
currentLongitudinalOffset = Props.longitudinalInitialOffset;
|
||||
}
|
||||
|
||||
Log.Message($"GroundStrafing: Initialized with {confirmedTargetCells.Count} targets, " +
|
||||
$"Lateral Offset: {currentLateralOffsetAngle:F1}°, " +
|
||||
$"Longitudinal Offset: {currentLongitudinalOffset:F1}");
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
if (confirmedTargetCells.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CheckAndFireAtTargets();
|
||||
|
||||
// 定期状态输出
|
||||
if (Find.TickManager.TicksGame % 120 == 0 && confirmedTargetCells.Count > 0)
|
||||
{
|
||||
Log.Message($"GroundStrafing: {firedCells.Count}/{confirmedTargetCells.Count + firedCells.Count} targets fired, " +
|
||||
$"Lateral: {currentLateralOffsetAngle:F1}°, Longitudinal: {currentLongitudinalOffset:F1}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckAndFireAtTargets()
|
||||
{
|
||||
Vector3 currentPos = parent.DrawPos;
|
||||
|
||||
for (int i = confirmedTargetCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
IntVec3 targetCell = confirmedTargetCells[i];
|
||||
|
||||
if (firedCells.Contains(targetCell))
|
||||
{
|
||||
confirmedTargetCells.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
float horizontalDistance = GetHorizontalDistance(currentPos, targetCell);
|
||||
if (horizontalDistance <= Props.range)
|
||||
{
|
||||
if (LaunchProjectileAt(targetCell))
|
||||
{
|
||||
firedCells.Add(targetCell);
|
||||
confirmedTargetCells.RemoveAt(i);
|
||||
|
||||
// 更新所有偏移参数
|
||||
UpdateOffsets();
|
||||
|
||||
if (firedCells.Count == 1)
|
||||
{
|
||||
Log.Message($"First strafing shot at {targetCell}, " +
|
||||
$"Lateral offset: {currentLateralOffsetAngle:F1}°, " +
|
||||
$"Longitudinal offset: {currentLongitudinalOffset:F1}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:更新所有偏移参数
|
||||
private void UpdateOffsets()
|
||||
{
|
||||
shotsFired++;
|
||||
|
||||
// 更新横向偏移
|
||||
UpdateLateralOffset();
|
||||
|
||||
// 更新纵向偏移
|
||||
UpdateLongitudinalOffset();
|
||||
}
|
||||
|
||||
// 横向偏移逻辑(左右)
|
||||
private void UpdateLateralOffset()
|
||||
{
|
||||
switch (Props.lateralOffsetMode)
|
||||
{
|
||||
case OffsetMode.Alternating:
|
||||
currentLateralOffsetAngle = (shotsFired % 2 == 0) ? Props.lateralOffsetDistance : -Props.lateralOffsetDistance;
|
||||
break;
|
||||
|
||||
case OffsetMode.Progressive:
|
||||
currentLateralOffsetAngle += Props.lateralAngleIncrement;
|
||||
if (Mathf.Abs(currentLateralOffsetAngle) > Props.lateralMaxOffsetAngle)
|
||||
{
|
||||
currentLateralOffsetAngle = Props.lateralInitialOffsetAngle;
|
||||
}
|
||||
break;
|
||||
|
||||
case OffsetMode.Random:
|
||||
currentLateralOffsetAngle = Random.Range(-Props.lateralMaxOffsetAngle, Props.lateralMaxOffsetAngle);
|
||||
break;
|
||||
|
||||
case OffsetMode.Fixed:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (Props.lateralMaxOffsetAngle > 0)
|
||||
{
|
||||
currentLateralOffsetAngle = Mathf.Clamp(currentLateralOffsetAngle, -Props.lateralMaxOffsetAngle, Props.lateralMaxOffsetAngle);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:纵向偏移逻辑(前后)
|
||||
private void UpdateLongitudinalOffset()
|
||||
{
|
||||
switch (Props.longitudinalOffsetMode)
|
||||
{
|
||||
case LongitudinalOffsetMode.Alternating:
|
||||
// 交替模式:前后交替
|
||||
currentLongitudinalOffset = (shotsFired % 2 == 0) ? Props.longitudinalAlternationAmplitude : -Props.longitudinalAlternationAmplitude;
|
||||
break;
|
||||
|
||||
case LongitudinalOffsetMode.Progressive:
|
||||
// 渐进模式:逐渐向前然后向后
|
||||
if (isForwardPhase)
|
||||
{
|
||||
currentLongitudinalOffset += Props.longitudinalProgressionStep;
|
||||
if (currentLongitudinalOffset >= Props.longitudinalMaxOffset)
|
||||
{
|
||||
isForwardPhase = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLongitudinalOffset -= Props.longitudinalProgressionStep;
|
||||
if (currentLongitudinalOffset <= Props.longitudinalMinOffset)
|
||||
{
|
||||
isForwardPhase = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case LongitudinalOffsetMode.Random:
|
||||
// 随机模式
|
||||
currentLongitudinalOffset = Random.Range(Props.longitudinalMinOffset, Props.longitudinalMaxOffset);
|
||||
break;
|
||||
|
||||
case LongitudinalOffsetMode.Sinusoidal:
|
||||
// 正弦波模式:平滑的前后波动
|
||||
float time = shotsFired * Props.longitudinalOscillationSpeed;
|
||||
currentLongitudinalOffset = Mathf.Sin(time) * Props.longitudinalOscillationAmplitude;
|
||||
break;
|
||||
|
||||
case LongitudinalOffsetMode.Fixed:
|
||||
default:
|
||||
// 固定模式:保持不变
|
||||
break;
|
||||
}
|
||||
|
||||
// 应用限制
|
||||
currentLongitudinalOffset = Mathf.Clamp(currentLongitudinalOffset, Props.longitudinalMinOffset, Props.longitudinalMaxOffset);
|
||||
}
|
||||
|
||||
// 修改:计算包含横向和纵向偏移的发射位置
|
||||
private Vector3 CalculateOffsetPosition(Vector3 basePosition, Vector3 directionToTarget)
|
||||
{
|
||||
Vector3 finalPosition = basePosition;
|
||||
|
||||
// 应用横向偏移(左右)
|
||||
if (Mathf.Abs(currentLateralOffsetAngle) > 0.01f)
|
||||
{
|
||||
Vector3 flyDirection = GetFlyOverDirection();
|
||||
Vector3 perpendicular = Vector3.Cross(flyDirection, Vector3.up).normalized;
|
||||
float lateralOffsetDistance = Props.lateralOffsetDistance;
|
||||
Vector3 lateralOffset = perpendicular * lateralOffsetDistance * Mathf.Sin(currentLateralOffsetAngle * Mathf.Deg2Rad);
|
||||
finalPosition += lateralOffset;
|
||||
}
|
||||
|
||||
// 应用纵向偏移(前后)
|
||||
if (Mathf.Abs(currentLongitudinalOffset) > 0.01f)
|
||||
{
|
||||
Vector3 flyDirection = GetFlyOverDirection();
|
||||
Vector3 longitudinalOffset = flyDirection * currentLongitudinalOffset;
|
||||
finalPosition += longitudinalOffset;
|
||||
}
|
||||
|
||||
return finalPosition;
|
||||
}
|
||||
|
||||
private Vector3 GetFlyOverDirection()
|
||||
{
|
||||
FlyOver flyOver = parent as FlyOver;
|
||||
if (flyOver != null)
|
||||
{
|
||||
return flyOver.MovementDirection;
|
||||
}
|
||||
return Vector3.forward;
|
||||
}
|
||||
|
||||
private float GetHorizontalDistance(Vector3 fromPos, IntVec3 toCell)
|
||||
{
|
||||
Vector2 fromPos2D = new Vector2(fromPos.x, fromPos.z);
|
||||
Vector2 toPos2D = new Vector2(toCell.x, toCell.z);
|
||||
return Vector2.Distance(fromPos2D, toPos2D);
|
||||
}
|
||||
|
||||
private bool LaunchProjectileAt(IntVec3 targetCell)
|
||||
{
|
||||
if (Props.projectileDef == null)
|
||||
{
|
||||
Log.Error("No projectile defined for ground strafing");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Vector3 spawnPos = parent.DrawPos;
|
||||
Vector3 targetPos = targetCell.ToVector3();
|
||||
Vector3 directionToTarget = (targetPos - spawnPos).normalized;
|
||||
|
||||
// 计算偏移后的发射位置
|
||||
Vector3 offsetSpawnPos = CalculateOffsetPosition(spawnPos, directionToTarget);
|
||||
|
||||
IntVec3 spawnCell = offsetSpawnPos.ToIntVec3();
|
||||
|
||||
// 创建抛射体
|
||||
Projectile projectile = (Projectile)GenSpawn.Spawn(Props.projectileDef, spawnCell, parent.Map);
|
||||
|
||||
if (projectile != null)
|
||||
{
|
||||
Thing launcher = GetLauncher();
|
||||
lastProjectileDirection = directionToTarget;
|
||||
|
||||
// 发射抛射体
|
||||
projectile.Launch(
|
||||
launcher,
|
||||
offsetSpawnPos,
|
||||
new LocalTargetInfo(targetCell),
|
||||
new LocalTargetInfo(targetCell),
|
||||
ProjectileHitFlags.IntendedTarget,
|
||||
false
|
||||
);
|
||||
|
||||
// 播放偏移特效
|
||||
if (Props.spawnOffsetEffect)
|
||||
{
|
||||
CreateOffsetEffect(offsetSpawnPos, directionToTarget);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"Error launching ground strafing projectile: {ex}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 偏移特效
|
||||
private void CreateOffsetEffect(Vector3 spawnPos, Vector3 direction)
|
||||
{
|
||||
if (Props.offsetEffectDef != null)
|
||||
{
|
||||
MoteMaker.MakeStaticMote(
|
||||
spawnPos,
|
||||
parent.Map,
|
||||
Props.offsetEffectDef,
|
||||
1f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Thing GetLauncher()
|
||||
{
|
||||
FlyOver flyOver = parent as FlyOver;
|
||||
if (flyOver != null && flyOver.caster != null)
|
||||
{
|
||||
return flyOver.caster;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
public void SetConfirmedTargets(List<IntVec3> targets)
|
||||
{
|
||||
confirmedTargetCells.Clear();
|
||||
firedCells.Clear();
|
||||
shotsFired = 0;
|
||||
currentLateralOffsetAngle = Props.lateralInitialOffsetAngle;
|
||||
currentLongitudinalOffset = Props.longitudinalInitialOffset;
|
||||
isForwardPhase = true;
|
||||
|
||||
confirmedTargetCells.AddRange(targets);
|
||||
|
||||
Log.Message($"GroundStrafing: Set {confirmedTargetCells.Count} targets, " +
|
||||
$"Lateral Mode: {Props.lateralOffsetMode}, " +
|
||||
$"Longitudinal Mode: {Props.longitudinalOffsetMode}");
|
||||
|
||||
if (confirmedTargetCells.Count > 0)
|
||||
{
|
||||
Log.Message($"First target: {confirmedTargetCells[0]}, Last target: {confirmedTargetCells[confirmedTargetCells.Count - 1]}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
|
||||
Scribe_Collections.Look(ref confirmedTargetCells, "confirmedTargetCells", LookMode.Value);
|
||||
Scribe_Collections.Look(ref firedCells, "firedCells", LookMode.Value);
|
||||
Scribe_Values.Look(ref currentLateralOffsetAngle, "currentLateralOffsetAngle", Props.lateralInitialOffsetAngle);
|
||||
Scribe_Values.Look(ref currentLongitudinalOffset, "currentLongitudinalOffset", Props.longitudinalInitialOffset);
|
||||
Scribe_Values.Look(ref shotsFired, "shotsFired", 0);
|
||||
Scribe_Values.Look(ref isForwardPhase, "isForwardPhase", true);
|
||||
}
|
||||
|
||||
// 修改:调试方法
|
||||
public void DebugOffsetStatus()
|
||||
{
|
||||
Log.Message($"GroundStrafing Offset Status:");
|
||||
Log.Message($" Lateral - Angle: {currentLateralOffsetAngle:F1}°, Mode: {Props.lateralOffsetMode}");
|
||||
Log.Message($" Longitudinal - Offset: {currentLongitudinalOffset:F1}, Mode: {Props.longitudinalOffsetMode}");
|
||||
Log.Message($" Shots Fired: {shotsFired}, Forward Phase: {isForwardPhase}");
|
||||
}
|
||||
}
|
||||
|
||||
public class CompProperties_GroundStrafing : CompProperties
|
||||
{
|
||||
public ThingDef projectileDef; // 抛射体定义
|
||||
public float range = 15f; // 射程
|
||||
|
||||
// 横向偏移配置(左右)
|
||||
public float lateralOffsetDistance = 2f;
|
||||
public float lateralInitialOffsetAngle = 0f;
|
||||
public float lateralMaxOffsetAngle = 45f;
|
||||
public float lateralAngleIncrement = 5f;
|
||||
public OffsetMode lateralOffsetMode = OffsetMode.Alternating;
|
||||
|
||||
// 纵向偏移配置(前后)
|
||||
public float longitudinalInitialOffset = 0f; // 初始纵向偏移
|
||||
public float longitudinalMinOffset = -2f; // 最小纵向偏移
|
||||
public float longitudinalMaxOffset = 2f; // 最大纵向偏移
|
||||
public LongitudinalOffsetMode longitudinalOffsetMode = LongitudinalOffsetMode.Alternating; // 纵向偏移模式
|
||||
|
||||
// 正弦波模式参数
|
||||
public float longitudinalOscillationSpeed = 0.5f; // 振荡速度
|
||||
public float longitudinalOscillationAmplitude = 1f; // 振荡幅度
|
||||
|
||||
// 交替模式参数
|
||||
public float longitudinalAlternationAmplitude = 1f; // 交替幅度
|
||||
|
||||
// 渐进模式参数
|
||||
public float longitudinalProgressionStep = 0.1f; // 渐进步长
|
||||
|
||||
// 视觉效果
|
||||
public bool spawnOffsetEffect = false;
|
||||
public ThingDef offsetEffectDef;
|
||||
|
||||
public CompProperties_GroundStrafing()
|
||||
{
|
||||
compClass = typeof(CompGroundStrafing);
|
||||
}
|
||||
}
|
||||
|
||||
// 横向偏移模式枚举
|
||||
public enum OffsetMode
|
||||
{
|
||||
Fixed,
|
||||
Alternating,
|
||||
Progressive,
|
||||
Random
|
||||
}
|
||||
|
||||
// 新增:纵向偏移模式枚举
|
||||
public enum LongitudinalOffsetMode
|
||||
{
|
||||
Fixed, // 固定
|
||||
Alternating, // 交替(前后交替)
|
||||
Progressive, // 渐进(逐渐变化)
|
||||
Random, // 随机
|
||||
Sinusoidal // 正弦波(平滑波动)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompSectorSurveillance : ThingComp
|
||||
{
|
||||
public CompProperties_SectorSurveillance Props => (CompProperties_SectorSurveillance)props;
|
||||
|
||||
// 监视状态
|
||||
private HashSet<Pawn> attackedPawns = new HashSet<Pawn>();
|
||||
private Dictionary<Pawn, int> activeTargets = new Dictionary<Pawn, int>();
|
||||
private Dictionary<Pawn, int> shotCooldowns = new Dictionary<Pawn, int>();
|
||||
|
||||
// 性能优化
|
||||
private int checkInterval = 10;
|
||||
private int lastCheckTick = 0;
|
||||
|
||||
// 调试状态
|
||||
private int totalFramesProcessed = 0;
|
||||
private int totalTargetsFound = 0;
|
||||
private int totalShotsFired = 0;
|
||||
|
||||
// 派系缓存
|
||||
private Faction cachedFaction = null;
|
||||
private bool factionInitialized = false;
|
||||
|
||||
// 射弹数量跟踪
|
||||
private int remainingProjectiles = -1; // -1 表示无限
|
||||
private bool ammoExhausted = false;
|
||||
|
||||
// 新增:横纵轴偏移状态
|
||||
private float currentLateralOffsetAngle = 0f; // 当前横向偏移角度
|
||||
private float currentLongitudinalOffset = 0f; // 当前纵向偏移距离
|
||||
private bool isForwardPhase = true; // 是否处于向前偏移阶段
|
||||
private int shotsFired = 0; // 总发射次数(用于偏移计算)
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
base.PostSpawnSetup(respawningAfterLoad);
|
||||
|
||||
// 初始化射弹数量
|
||||
if (!respawningAfterLoad)
|
||||
{
|
||||
remainingProjectiles = Props.maxProjectiles;
|
||||
ammoExhausted = false;
|
||||
|
||||
// 初始化偏移
|
||||
currentLateralOffsetAngle = Props.lateralInitialOffsetAngle;
|
||||
currentLongitudinalOffset = Props.longitudinalInitialOffset;
|
||||
}
|
||||
|
||||
Log.Message($"SectorSurveillance: Initialized - Angle: {Props.sectorAngle}°, Range: {Props.sectorRange}, Shots: {Props.shotCount}, Interval: {Props.shotInterval}s");
|
||||
Log.Message($"SectorSurveillance: ProjectileDef = {Props.projectileDef?.defName ?? "NULL"}");
|
||||
Log.Message($"SectorSurveillance: Parent = {parent?.def?.defName ?? "NULL"} at {parent?.Position.ToString() ?? "NULL"}");
|
||||
Log.Message($"SectorSurveillance: Max Projectiles = {Props.maxProjectiles}, Remaining = {remainingProjectiles}");
|
||||
Log.Message($"SectorSurveillance: Lateral Mode: {Props.lateralOffsetMode}, Longitudinal Mode: {Props.longitudinalOffsetMode}");
|
||||
|
||||
InitializeFactionCache();
|
||||
}
|
||||
|
||||
private void InitializeFactionCache()
|
||||
{
|
||||
Log.Message($"SectorSurveillance: Initializing faction cache...");
|
||||
|
||||
if (parent.Faction != null)
|
||||
{
|
||||
cachedFaction = parent.Faction;
|
||||
Log.Message($"SectorSurveillance: Using parent.Faction: {cachedFaction?.Name ?? "NULL"}");
|
||||
}
|
||||
else
|
||||
{
|
||||
FlyOver flyOver = parent as FlyOver;
|
||||
if (flyOver?.caster != null && flyOver.caster.Faction != null)
|
||||
{
|
||||
cachedFaction = flyOver.caster.Faction;
|
||||
Log.Message($"SectorSurveillance: Using caster.Faction: {cachedFaction?.Name ?? "NULL"}");
|
||||
}
|
||||
else if (flyOver?.faction != null)
|
||||
{
|
||||
cachedFaction = flyOver.faction;
|
||||
Log.Message($"SectorSurveillance: Using flyOver.faction: {cachedFaction?.Name ?? "NULL"}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"SectorSurveillance: CRITICAL - No faction found!");
|
||||
}
|
||||
}
|
||||
|
||||
factionInitialized = true;
|
||||
Log.Message($"SectorSurveillance: Faction cache initialized: {cachedFaction?.Name ?? "NULL"}");
|
||||
}
|
||||
|
||||
private Faction GetEffectiveFaction()
|
||||
{
|
||||
if (!factionInitialized)
|
||||
{
|
||||
InitializeFactionCache();
|
||||
}
|
||||
|
||||
if (cachedFaction == null)
|
||||
{
|
||||
Log.Warning("SectorSurveillance: Cached faction is null, reinitializing...");
|
||||
InitializeFactionCache();
|
||||
}
|
||||
|
||||
return cachedFaction;
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
totalFramesProcessed++;
|
||||
|
||||
// 每60帧输出一次状态摘要
|
||||
if (Find.TickManager.TicksGame % 60 == 0)
|
||||
{
|
||||
Faction currentFaction = GetEffectiveFaction();
|
||||
Log.Message($"SectorSurveillance Status: Frames={totalFramesProcessed}, TargetsFound={totalTargetsFound}, ShotsFired={totalShotsFired}, ActiveTargets={activeTargets.Count}, Cooldowns={shotCooldowns.Count}, Faction={currentFaction?.Name ?? "NULL"}, RemainingProjectiles={remainingProjectiles}, AmmoExhausted={ammoExhausted}");
|
||||
Log.Message($"SectorSurveillance Offsets: Lateral={currentLateralOffsetAngle:F1}°, Longitudinal={currentLongitudinalOffset:F1}, TotalShots={shotsFired}");
|
||||
}
|
||||
|
||||
UpdateShotCooldowns();
|
||||
|
||||
if (Find.TickManager.TicksGame - lastCheckTick >= checkInterval)
|
||||
{
|
||||
CheckSectorForTargets();
|
||||
lastCheckTick = Find.TickManager.TicksGame;
|
||||
}
|
||||
|
||||
ExecuteAttacks();
|
||||
}
|
||||
|
||||
private void UpdateShotCooldowns()
|
||||
{
|
||||
List<Pawn> toRemove = new List<Pawn>();
|
||||
|
||||
// 关键修复:创建键的副本
|
||||
List<Pawn> cooldownKeys = new List<Pawn>(shotCooldowns.Keys);
|
||||
|
||||
foreach (Pawn pawn in cooldownKeys)
|
||||
{
|
||||
// 检查pawn是否仍然有效(可能已被爆炸杀死)
|
||||
if (pawn == null || pawn.Destroyed || pawn.Dead || !pawn.Spawned)
|
||||
{
|
||||
toRemove.Add(pawn);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查键是否仍在字典中
|
||||
if (!shotCooldowns.ContainsKey(pawn))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
shotCooldowns[pawn]--;
|
||||
if (shotCooldowns[pawn] <= 0)
|
||||
{
|
||||
toRemove.Add(pawn);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Pawn pawn in toRemove)
|
||||
{
|
||||
shotCooldowns.Remove(pawn);
|
||||
Log.Message($"SectorSurveillance: Cooldown finished for {pawn?.Label ?? "NULL"}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckSectorForTargets()
|
||||
{
|
||||
// 如果弹药耗尽,不再检查新目标
|
||||
if (ammoExhausted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<Pawn> enemiesInSector = GetEnemiesInSector();
|
||||
Log.Message($"SectorSurveillance: Found {enemiesInSector.Count} enemies in sector");
|
||||
|
||||
if (enemiesInSector.Count > 0)
|
||||
{
|
||||
Log.Message($"SectorSurveillance: Enemies in sector: {string.Join(", ", enemiesInSector.ConvertAll(p => p.Label))}");
|
||||
}
|
||||
|
||||
foreach (Pawn enemy in enemiesInSector)
|
||||
{
|
||||
totalTargetsFound++;
|
||||
|
||||
if (!attackedPawns.Contains(enemy) &&
|
||||
!activeTargets.ContainsKey(enemy) &&
|
||||
!shotCooldowns.ContainsKey(enemy))
|
||||
{
|
||||
activeTargets[enemy] = Props.shotCount;
|
||||
Log.Message($"SectorSurveillance: Starting attack sequence on {enemy.Label} at {enemy.Position} - {Props.shotCount} shots");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteAttacks()
|
||||
{
|
||||
// 如果弹药耗尽,不再执行攻击
|
||||
if (ammoExhausted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<Pawn> completedTargets = new List<Pawn>();
|
||||
|
||||
// 关键修复:在枚举之前创建键的副本
|
||||
List<Pawn> targetsToProcess = new List<Pawn>(activeTargets.Keys);
|
||||
|
||||
foreach (Pawn enemy in targetsToProcess)
|
||||
{
|
||||
// 检查目标是否仍然有效(可能已被爆炸杀死)
|
||||
if (enemy == null || enemy.Destroyed || enemy.Dead || !enemy.Spawned)
|
||||
{
|
||||
completedTargets.Add(enemy);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查目标是否仍在字典中
|
||||
if (!activeTargets.ContainsKey(enemy))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int remainingShots = activeTargets[enemy];
|
||||
|
||||
if (!IsInSector(enemy.Position))
|
||||
{
|
||||
Log.Message($"SectorSurveillance: Target {enemy.Label} left sector, cancelling attack");
|
||||
completedTargets.Add(enemy);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shotCooldowns.ContainsKey(enemy))
|
||||
{
|
||||
Log.Message($"SectorSurveillance: Target {enemy.Label} in cooldown, skipping this frame");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查剩余射弹数量
|
||||
if (remainingProjectiles == 0)
|
||||
{
|
||||
Log.Message($"SectorSurveillance: Ammo exhausted, cannot fire at {enemy.Label}");
|
||||
ammoExhausted = true;
|
||||
break; // 跳出循环,不再发射任何射弹
|
||||
}
|
||||
|
||||
Log.Message($"SectorSurveillance: Attempting to fire at {enemy.Label}, remaining shots: {remainingShots}, remaining projectiles: {remainingProjectiles}");
|
||||
if (LaunchProjectileAt(enemy))
|
||||
{
|
||||
totalShotsFired++;
|
||||
remainingShots--;
|
||||
activeTargets[enemy] = remainingShots;
|
||||
|
||||
// 减少剩余射弹数量(如果不是无限)
|
||||
if (remainingProjectiles > 0)
|
||||
{
|
||||
remainingProjectiles--;
|
||||
Log.Message($"SectorSurveillance: Remaining projectiles: {remainingProjectiles}");
|
||||
|
||||
// 检查是否耗尽弹药
|
||||
if (remainingProjectiles == 0)
|
||||
{
|
||||
ammoExhausted = true;
|
||||
Log.Message($"SectorSurveillance: AMMO EXHAUSTED - No more projectiles available");
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:更新偏移状态
|
||||
UpdateOffsets();
|
||||
|
||||
int cooldownTicks = Mathf.RoundToInt(Props.shotInterval * 60f);
|
||||
shotCooldowns[enemy] = cooldownTicks;
|
||||
|
||||
Log.Message($"SectorSurveillance: Successfully fired at {enemy.Label}, {remainingShots} shots remaining, cooldown: {cooldownTicks} ticks");
|
||||
|
||||
if (remainingShots <= 0)
|
||||
{
|
||||
attackedPawns.Add(enemy);
|
||||
completedTargets.Add(enemy);
|
||||
Log.Message($"SectorSurveillance: Completed attack sequence on {enemy.Label}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"SectorSurveillance: Failed to fire projectile at {enemy.Label}");
|
||||
}
|
||||
}
|
||||
|
||||
// 清理已完成的目标
|
||||
foreach (Pawn enemy in completedTargets)
|
||||
{
|
||||
// 再次检查目标是否有效
|
||||
if (enemy != null)
|
||||
{
|
||||
activeTargets.Remove(enemy);
|
||||
Log.Message($"SectorSurveillance: Removed {enemy.Label} from active targets");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果目标已不存在,直接从字典中移除对应的键
|
||||
activeTargets.Remove(enemy);
|
||||
Log.Message($"SectorSurveillance: Removed null target from active targets");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:更新所有偏移参数
|
||||
private void UpdateOffsets()
|
||||
{
|
||||
shotsFired++;
|
||||
|
||||
// 更新横向偏移
|
||||
UpdateLateralOffset();
|
||||
|
||||
// 更新纵向偏移
|
||||
UpdateLongitudinalOffset();
|
||||
}
|
||||
|
||||
// 横向偏移逻辑(左右)
|
||||
private void UpdateLateralOffset()
|
||||
{
|
||||
switch (Props.lateralOffsetMode)
|
||||
{
|
||||
case OffsetMode.Alternating:
|
||||
currentLateralOffsetAngle = (shotsFired % 2 == 0) ? Props.lateralOffsetDistance : -Props.lateralOffsetDistance;
|
||||
break;
|
||||
|
||||
case OffsetMode.Progressive:
|
||||
currentLateralOffsetAngle += Props.lateralAngleIncrement;
|
||||
if (Mathf.Abs(currentLateralOffsetAngle) > Props.lateralMaxOffsetAngle)
|
||||
{
|
||||
currentLateralOffsetAngle = Props.lateralInitialOffsetAngle;
|
||||
}
|
||||
break;
|
||||
|
||||
case OffsetMode.Random:
|
||||
currentLateralOffsetAngle = Random.Range(-Props.lateralMaxOffsetAngle, Props.lateralMaxOffsetAngle);
|
||||
break;
|
||||
|
||||
case OffsetMode.Fixed:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (Props.lateralMaxOffsetAngle > 0)
|
||||
{
|
||||
currentLateralOffsetAngle = Mathf.Clamp(currentLateralOffsetAngle, -Props.lateralMaxOffsetAngle, Props.lateralMaxOffsetAngle);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:纵向偏移逻辑(前后)
|
||||
private void UpdateLongitudinalOffset()
|
||||
{
|
||||
switch (Props.longitudinalOffsetMode)
|
||||
{
|
||||
case LongitudinalOffsetMode.Alternating:
|
||||
// 交替模式:前后交替
|
||||
currentLongitudinalOffset = (shotsFired % 2 == 0) ? Props.longitudinalAlternationAmplitude : -Props.longitudinalAlternationAmplitude;
|
||||
break;
|
||||
|
||||
case LongitudinalOffsetMode.Progressive:
|
||||
// 渐进模式:逐渐向前然后向后
|
||||
if (isForwardPhase)
|
||||
{
|
||||
currentLongitudinalOffset += Props.longitudinalProgressionStep;
|
||||
if (currentLongitudinalOffset >= Props.longitudinalMaxOffset)
|
||||
{
|
||||
isForwardPhase = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLongitudinalOffset -= Props.longitudinalProgressionStep;
|
||||
if (currentLongitudinalOffset <= Props.longitudinalMinOffset)
|
||||
{
|
||||
isForwardPhase = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case LongitudinalOffsetMode.Random:
|
||||
// 随机模式
|
||||
currentLongitudinalOffset = Random.Range(Props.longitudinalMinOffset, Props.longitudinalMaxOffset);
|
||||
break;
|
||||
|
||||
case LongitudinalOffsetMode.Sinusoidal:
|
||||
// 正弦波模式:平滑的前后波动
|
||||
float time = shotsFired * Props.longitudinalOscillationSpeed;
|
||||
currentLongitudinalOffset = Mathf.Sin(time) * Props.longitudinalOscillationAmplitude;
|
||||
break;
|
||||
|
||||
case LongitudinalOffsetMode.Fixed:
|
||||
default:
|
||||
// 固定模式:保持不变
|
||||
break;
|
||||
}
|
||||
|
||||
// 应用限制
|
||||
currentLongitudinalOffset = Mathf.Clamp(currentLongitudinalOffset, Props.longitudinalMinOffset, Props.longitudinalMaxOffset);
|
||||
}
|
||||
|
||||
// 新增:计算包含横向和纵向偏移的发射位置
|
||||
private Vector3 CalculateOffsetPosition(Vector3 basePosition, Vector3 directionToTarget)
|
||||
{
|
||||
Vector3 finalPosition = basePosition;
|
||||
|
||||
// 应用横向偏移(左右)
|
||||
if (Mathf.Abs(currentLateralOffsetAngle) > 0.01f)
|
||||
{
|
||||
Vector3 flyDirection = GetFlyOverDirection();
|
||||
Vector3 perpendicular = Vector3.Cross(flyDirection, Vector3.up).normalized;
|
||||
float lateralOffsetDistance = Props.lateralOffsetDistance;
|
||||
Vector3 lateralOffset = perpendicular * lateralOffsetDistance * Mathf.Sin(currentLateralOffsetAngle * Mathf.Deg2Rad);
|
||||
finalPosition += lateralOffset;
|
||||
}
|
||||
|
||||
// 应用纵向偏移(前后)
|
||||
if (Mathf.Abs(currentLongitudinalOffset) > 0.01f)
|
||||
{
|
||||
Vector3 flyDirection = GetFlyOverDirection();
|
||||
Vector3 longitudinalOffset = flyDirection * currentLongitudinalOffset;
|
||||
finalPosition += longitudinalOffset;
|
||||
}
|
||||
|
||||
return finalPosition;
|
||||
}
|
||||
|
||||
private Vector3 GetFlyOverDirection()
|
||||
{
|
||||
FlyOver flyOver = parent as FlyOver;
|
||||
if (flyOver != null)
|
||||
{
|
||||
return flyOver.MovementDirection;
|
||||
}
|
||||
return Vector3.forward;
|
||||
}
|
||||
|
||||
private List<Pawn> GetEnemiesInSector()
|
||||
{
|
||||
List<Pawn> enemies = new List<Pawn>();
|
||||
Map map = parent.Map;
|
||||
|
||||
if (map == null)
|
||||
{
|
||||
Log.Error("SectorSurveillance: Map is null!");
|
||||
return enemies;
|
||||
}
|
||||
|
||||
FlyOver flyOver = parent as FlyOver;
|
||||
if (flyOver == null)
|
||||
{
|
||||
Log.Error("SectorSurveillance: Parent is not a FlyOver!");
|
||||
return enemies;
|
||||
}
|
||||
|
||||
Vector3 center = parent.DrawPos;
|
||||
Vector3 flightDirection = flyOver.MovementDirection;
|
||||
float range = Props.sectorRange;
|
||||
float halfAngle = Props.sectorAngle * 0.5f;
|
||||
|
||||
Log.Message($"SectorSurveillance: Checking sector - Center: {center}, Direction: {flightDirection}, Range: {range}, HalfAngle: {halfAngle}");
|
||||
|
||||
int totalEnemiesChecked = 0;
|
||||
|
||||
// 关键修复:创建pawn列表的副本,避免在枚举时集合被修改
|
||||
List<Pawn> allPawns = new List<Pawn>(map.mapPawns.AllPawnsSpawned);
|
||||
|
||||
foreach (Pawn pawn in allPawns)
|
||||
{
|
||||
totalEnemiesChecked++;
|
||||
if (IsValidTarget(pawn))
|
||||
{
|
||||
bool inSector = IsInSector(pawn.Position);
|
||||
if (inSector)
|
||||
{
|
||||
enemies.Add(pawn);
|
||||
Log.Message($"SectorSurveillance: Valid target found - {pawn.Label} at {pawn.Position}, in sector: {inSector}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message($"SectorSurveillance: Checked {totalEnemiesChecked} pawns, found {enemies.Count} valid targets in sector");
|
||||
return enemies;
|
||||
}
|
||||
|
||||
private bool IsValidTarget(Pawn pawn)
|
||||
{
|
||||
if (pawn == null)
|
||||
{
|
||||
Log.Message("SectorSurveillance: IsValidTarget - pawn is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 关键修复:检查pawn是否已被销毁或死亡
|
||||
if (pawn.Destroyed || pawn.Dead || !pawn.Spawned)
|
||||
{
|
||||
Log.Message($"SectorSurveillance: IsValidTarget - {pawn.Label} is destroyed/dead/unspawned");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pawn.Downed)
|
||||
{
|
||||
Log.Message($"SectorSurveillance: IsValidTarget - {pawn.Label} is downed");
|
||||
return false;
|
||||
}
|
||||
|
||||
Faction effectiveFaction = GetEffectiveFaction();
|
||||
if (effectiveFaction == null)
|
||||
{
|
||||
Log.Error($"SectorSurveillance: IsValidTarget - No effective faction found for {pawn.Label}");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hostile = pawn.HostileTo(effectiveFaction);
|
||||
Log.Message($"SectorSurveillance: IsValidTarget - {pawn.Label} from {pawn.Faction?.Name ?? "NULL"} is hostile to {effectiveFaction.Name}: {hostile}");
|
||||
|
||||
return hostile;
|
||||
}
|
||||
|
||||
private bool IsInSector(IntVec3 targetPos)
|
||||
{
|
||||
FlyOver flyOver = parent as FlyOver;
|
||||
if (flyOver == null)
|
||||
{
|
||||
Log.Error("SectorSurveillance: IsInSector - Parent is not a FlyOver!");
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector3 flyOverPos = parent.DrawPos;
|
||||
Vector3 targetVector = targetPos.ToVector3() - flyOverPos;
|
||||
targetVector.y = 0;
|
||||
|
||||
float distance = targetVector.magnitude;
|
||||
if (distance > Props.sectorRange)
|
||||
{
|
||||
Log.Message($"SectorSurveillance: IsInSector - Target at {targetPos} is out of range: {distance:F1} > {Props.sectorRange}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector3 flightDirection = flyOver.MovementDirection;
|
||||
float angle = Vector3.Angle(flightDirection, targetVector);
|
||||
|
||||
bool inAngle = angle <= Props.sectorAngle * 0.5f;
|
||||
|
||||
Log.Message($"SectorSurveillance: IsInSector - Target at {targetPos}, distance: {distance:F1}, angle: {angle:F1}°, inAngle: {inAngle}");
|
||||
|
||||
return inAngle;
|
||||
}
|
||||
|
||||
private bool LaunchProjectileAt(Pawn target)
|
||||
{
|
||||
if (Props.projectileDef == null)
|
||||
{
|
||||
Log.Error("SectorSurveillance: No projectile defined for sector surveillance");
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.Message($"SectorSurveillance: LaunchProjectileAt - Starting launch for target {target?.Label ?? "NULL"}");
|
||||
|
||||
try
|
||||
{
|
||||
Vector3 spawnPos = parent.DrawPos;
|
||||
Vector3 targetPos = target.Position.ToVector3();
|
||||
Vector3 directionToTarget = (targetPos - spawnPos).normalized;
|
||||
|
||||
// 计算偏移后的发射位置
|
||||
Vector3 offsetSpawnPos = CalculateOffsetPosition(spawnPos, directionToTarget);
|
||||
|
||||
IntVec3 spawnCell = offsetSpawnPos.ToIntVec3();
|
||||
|
||||
Log.Message($"SectorSurveillance: Spawn position - World: {offsetSpawnPos}, Cell: {spawnCell}, Lateral Offset: {currentLateralOffsetAngle:F1}°, Longitudinal Offset: {currentLongitudinalOffset:F1}");
|
||||
|
||||
if (parent.Map == null)
|
||||
{
|
||||
Log.Error("SectorSurveillance: Map is null during projectile launch");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!spawnCell.InBounds(parent.Map))
|
||||
{
|
||||
Log.Error($"SectorSurveillance: Spawn cell {spawnCell} is out of bounds");
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.Message($"SectorSurveillance: Attempting to spawn projectile: {Props.projectileDef.defName}");
|
||||
Projectile projectile = (Projectile)GenSpawn.Spawn(Props.projectileDef, spawnCell, parent.Map);
|
||||
|
||||
if (projectile != null)
|
||||
{
|
||||
Log.Message($"SectorSurveillance: Projectile spawned successfully: {projectile}");
|
||||
|
||||
Thing launcher = GetLauncher();
|
||||
Vector3 launchPos = offsetSpawnPos;
|
||||
|
||||
LocalTargetInfo targetInfo = new LocalTargetInfo(target);
|
||||
|
||||
Log.Message($"SectorSurveillance: Launching projectile - Launcher: {launcher?.def?.defName ?? "NULL"}, LaunchPos: {launchPos}, Target: {targetInfo.Cell}");
|
||||
|
||||
projectile.Launch(
|
||||
launcher,
|
||||
launchPos,
|
||||
targetInfo,
|
||||
targetInfo,
|
||||
ProjectileHitFlags.IntendedTarget,
|
||||
false
|
||||
);
|
||||
|
||||
// 播放偏移特效
|
||||
if (Props.spawnOffsetEffect)
|
||||
{
|
||||
CreateOffsetEffect(offsetSpawnPos, directionToTarget);
|
||||
}
|
||||
|
||||
Log.Message($"SectorSurveillance: Projectile launched successfully");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("SectorSurveillance: Failed to spawn projectile - GenSpawn.Spawn returned null");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"SectorSurveillance: Exception launching projectile: {ex}");
|
||||
Log.Error($"SectorSurveillance: Stack trace: {ex.StackTrace}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 偏移特效
|
||||
private void CreateOffsetEffect(Vector3 spawnPos, Vector3 direction)
|
||||
{
|
||||
if (Props.offsetEffectDef != null)
|
||||
{
|
||||
MoteMaker.MakeStaticMote(
|
||||
spawnPos,
|
||||
parent.Map,
|
||||
Props.offsetEffectDef,
|
||||
1f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Thing GetLauncher()
|
||||
{
|
||||
FlyOver flyOver = parent as FlyOver;
|
||||
if (flyOver != null && flyOver.caster != null)
|
||||
{
|
||||
Log.Message($"SectorSurveillance: Using caster as launcher: {flyOver.caster.Label}");
|
||||
return flyOver.caster;
|
||||
}
|
||||
|
||||
Log.Message($"SectorSurveillance: Using parent as launcher: {parent.Label}");
|
||||
return parent;
|
||||
}
|
||||
|
||||
// 获取剩余射弹数量的方法(用于UI显示等)
|
||||
public int GetRemainingProjectiles()
|
||||
{
|
||||
return remainingProjectiles;
|
||||
}
|
||||
|
||||
// 检查是否还有弹药
|
||||
public bool HasAmmo()
|
||||
{
|
||||
return !ammoExhausted;
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
|
||||
Scribe_Collections.Look(ref attackedPawns, "attackedPawns", LookMode.Reference);
|
||||
Scribe_Collections.Look(ref activeTargets, "activeTargets", LookMode.Reference, LookMode.Value);
|
||||
Scribe_Collections.Look(ref shotCooldowns, "shotCooldowns", LookMode.Reference, LookMode.Value);
|
||||
Scribe_Values.Look(ref lastCheckTick, "lastCheckTick", 0);
|
||||
Scribe_Values.Look(ref totalFramesProcessed, "totalFramesProcessed", 0);
|
||||
Scribe_Values.Look(ref totalTargetsFound, "totalTargetsFound", 0);
|
||||
Scribe_Values.Look(ref totalShotsFired, "totalShotsFired", 0);
|
||||
Scribe_References.Look(ref cachedFaction, "cachedFaction");
|
||||
Scribe_Values.Look(ref factionInitialized, "factionInitialized", false);
|
||||
|
||||
// 保存和加载射弹数量状态
|
||||
Scribe_Values.Look(ref remainingProjectiles, "remainingProjectiles", -1);
|
||||
Scribe_Values.Look(ref ammoExhausted, "ammoExhausted", false);
|
||||
|
||||
// 新增:保存和加载偏移状态
|
||||
Scribe_Values.Look(ref currentLateralOffsetAngle, "currentLateralOffsetAngle", Props.lateralInitialOffsetAngle);
|
||||
Scribe_Values.Look(ref currentLongitudinalOffset, "currentLongitudinalOffset", Props.longitudinalInitialOffset);
|
||||
Scribe_Values.Look(ref shotsFired, "shotsFired", 0);
|
||||
Scribe_Values.Look(ref isForwardPhase, "isForwardPhase", true);
|
||||
}
|
||||
|
||||
public override string CompInspectStringExtra()
|
||||
{
|
||||
string baseString = base.CompInspectStringExtra();
|
||||
string ammoString = "";
|
||||
|
||||
if (Props.maxProjectiles == -1)
|
||||
{
|
||||
ammoString = "Ammo: Unlimited";
|
||||
}
|
||||
else
|
||||
{
|
||||
ammoString = $"Ammo: {remainingProjectiles}/{Props.maxProjectiles}";
|
||||
if (ammoExhausted)
|
||||
{
|
||||
ammoString += " (EXHAUSTED)";
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:显示偏移状态
|
||||
string offsetString = $"Offsets: Lateral {currentLateralOffsetAngle:F1}°, Longitudinal {currentLongitudinalOffset:F1}";
|
||||
|
||||
string result = ammoString + "\n" + offsetString;
|
||||
|
||||
if (!string.IsNullOrEmpty(baseString))
|
||||
{
|
||||
result = baseString + "\n" + result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 新增:调试方法
|
||||
public void DebugOffsetStatus()
|
||||
{
|
||||
Log.Message($"SectorSurveillance Offset Status:");
|
||||
Log.Message($" Lateral - Angle: {currentLateralOffsetAngle:F1}°, Mode: {Props.lateralOffsetMode}");
|
||||
Log.Message($" Longitudinal - Offset: {currentLongitudinalOffset:F1}, Mode: {Props.longitudinalOffsetMode}");
|
||||
Log.Message($" Shots Fired: {shotsFired}, Forward Phase: {isForwardPhase}");
|
||||
}
|
||||
}
|
||||
|
||||
public class CompProperties_SectorSurveillance : CompProperties
|
||||
{
|
||||
public ThingDef projectileDef;
|
||||
public float sectorAngle = 90f;
|
||||
public float sectorRange = 25f;
|
||||
public int shotCount = 3;
|
||||
public float shotInterval = 0.3f;
|
||||
|
||||
// 最大射弹数量限制
|
||||
public int maxProjectiles = -1; // -1 表示无限开火
|
||||
|
||||
// 新增:横纵轴偏移配置
|
||||
public float lateralOffsetDistance = 2f;
|
||||
public float lateralInitialOffsetAngle = 0f;
|
||||
public float lateralMaxOffsetAngle = 45f;
|
||||
public float lateralAngleIncrement = 5f;
|
||||
public OffsetMode lateralOffsetMode = OffsetMode.Alternating;
|
||||
|
||||
// 纵向偏移配置(前后)
|
||||
public float longitudinalInitialOffset = 0f; // 初始纵向偏移
|
||||
public float longitudinalMinOffset = -2f; // 最小纵向偏移
|
||||
public float longitudinalMaxOffset = 2f; // 最大纵向偏移
|
||||
public LongitudinalOffsetMode longitudinalOffsetMode = LongitudinalOffsetMode.Alternating; // 纵向偏移模式
|
||||
|
||||
// 正弦波模式参数
|
||||
public float longitudinalOscillationSpeed = 0.5f; // 振荡速度
|
||||
public float longitudinalOscillationAmplitude = 1f; // 振荡幅度
|
||||
|
||||
// 交替模式参数
|
||||
public float longitudinalAlternationAmplitude = 1f; // 交替幅度
|
||||
|
||||
// 渐进模式参数
|
||||
public float longitudinalProgressionStep = 0.1f; // 渐进步长
|
||||
|
||||
// 视觉效果
|
||||
public bool spawnOffsetEffect = false;
|
||||
public ThingDef offsetEffectDef;
|
||||
|
||||
public CompProperties_SectorSurveillance()
|
||||
{
|
||||
compClass = typeof(CompSectorSurveillance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_SendLetterAfterTicks : CompProperties
|
||||
{
|
||||
public int ticksDelay = 600; // 默认10秒 (60 ticks/秒)
|
||||
public string letterLabel;
|
||||
public string letterText;
|
||||
public LetterDef letterDef = LetterDefOf.NeutralEvent;
|
||||
public bool onlySendOnce = true;
|
||||
public bool requireOnMap = true;
|
||||
public bool destroyAfterSending = false;
|
||||
|
||||
public CompProperties_SendLetterAfterTicks()
|
||||
{
|
||||
compClass = typeof(CompSendLetterAfterTicks);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompSendLetterAfterTicks : ThingComp
|
||||
{
|
||||
public CompProperties_SendLetterAfterTicks Props => (CompProperties_SendLetterAfterTicks)props;
|
||||
|
||||
private int ticksPassed = 0;
|
||||
private bool letterSent = false;
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
// 如果已经发送过且只发送一次,则跳过
|
||||
if (letterSent && Props.onlySendOnce)
|
||||
return;
|
||||
|
||||
// 如果需要在地图上但父物体不在有效地图上,则跳过
|
||||
if (Props.requireOnMap && (parent.Map == null || !parent.Spawned))
|
||||
return;
|
||||
|
||||
ticksPassed++;
|
||||
|
||||
// 检查是否达到延迟时间
|
||||
if (ticksPassed >= Props.ticksDelay)
|
||||
{
|
||||
SendLetter();
|
||||
|
||||
if (Props.destroyAfterSending)
|
||||
{
|
||||
parent.Destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SendLetter()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查是否有有效的信件内容
|
||||
if (Props.letterLabel.NullOrEmpty() && Props.letterText.NullOrEmpty())
|
||||
{
|
||||
Log.Warning($"CompSendLetterAfterTicks: No letter content defined for {parent.def.defName}");
|
||||
return;
|
||||
}
|
||||
|
||||
string label = Props.letterLabel ?? "DefaultLetterLabel".Translate();
|
||||
string text = Props.letterText ?? "DefaultLetterText".Translate();
|
||||
|
||||
// 创建信件
|
||||
Letter letter = LetterMaker.MakeLetter(
|
||||
label,
|
||||
text,
|
||||
Props.letterDef,
|
||||
lookTargets: new LookTargets(parent)
|
||||
);
|
||||
|
||||
// 发送信件
|
||||
Find.LetterStack.ReceiveLetter(letter);
|
||||
|
||||
letterSent = true;
|
||||
|
||||
Log.Message($"Letter sent from {parent.def.defName} after {ticksPassed} ticks");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"Error sending letter from {parent.def.defName}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref ticksPassed, "ticksPassed", 0);
|
||||
Scribe_Values.Look(ref letterSent, "letterSent", false);
|
||||
}
|
||||
|
||||
public override string CompInspectStringExtra()
|
||||
{
|
||||
if (!letterSent && Props.requireOnMap && parent.Spawned)
|
||||
{
|
||||
int ticksRemaining = Props.ticksDelay - ticksPassed;
|
||||
if (ticksRemaining > 0)
|
||||
{
|
||||
return $"LetterInspection_TimeRemaining".Translate(ticksRemaining.ToStringTicksToPeriod());
|
||||
}
|
||||
}
|
||||
return base.CompInspectStringExtra();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_ShipArtillery : CompProperties
|
||||
{
|
||||
// 攻击配置
|
||||
public int ticksBetweenAttacks = 600; // 攻击间隔(tick)
|
||||
public int attackDurationTicks = 1800; // 攻击持续时间(tick)
|
||||
public int warmupTicks = 120; // 预热时间(tick)
|
||||
public bool continuousAttack = false; // 是否持续攻击直到飞越结束
|
||||
|
||||
// 目标区域配置
|
||||
public float attackRadius = 15f; // 攻击半径
|
||||
public IntVec3 targetOffset = IntVec3.Zero; // 目标偏移
|
||||
public bool useRandomTargets = true; // 是否使用随机目标
|
||||
public bool avoidPlayerAssets = true; // 是否避开玩家资产
|
||||
public float playerAssetAvoidanceRadius = 5f; // 避开玩家资产的半径
|
||||
|
||||
// 新增:无视保护机制的概率
|
||||
public float ignoreProtectionChance = 0f; // 0-1之间的值,0表示从不无视,1表示总是无视
|
||||
|
||||
// Skyfaller 配置
|
||||
public ThingDef skyfallerDef; // 使用的 Skyfaller 定义
|
||||
public List<ThingDef> skyfallerDefs; // 多个 Skyfaller 定义(随机选择)
|
||||
public int shellsPerVolley = 1; // 每轮齐射的炮弹数量
|
||||
public bool useDifferentShells = false; // 是否使用不同类型的炮弹
|
||||
|
||||
// 音效配置
|
||||
public SoundDef attackSound; // 攻击音效
|
||||
public SoundDef impactSound; // 撞击音效
|
||||
|
||||
// 视觉效果
|
||||
public EffecterDef warmupEffect; // 预热效果
|
||||
public EffecterDef attackEffect; // 攻击效果
|
||||
public FleckDef warmupFleck; // 预热粒子
|
||||
public FleckDef attackFleck; // 攻击粒子
|
||||
|
||||
// 避免击中飞越物体本身
|
||||
public bool avoidHittingFlyOver = true;
|
||||
|
||||
// 信件通知
|
||||
public bool sendAttackLetter = true; // 是否发送攻击信件
|
||||
public string customLetterLabel; // 自定义信件标题
|
||||
public string customLetterText; // 自定义信件内容
|
||||
public LetterDef letterDef = LetterDefOf.ThreatBig; // 信件类型
|
||||
|
||||
public CompProperties_ShipArtillery()
|
||||
{
|
||||
compClass = typeof(CompShipArtillery);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.Sound;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompShipArtillery : ThingComp
|
||||
{
|
||||
public CompProperties_ShipArtillery Props => (CompProperties_ShipArtillery)props;
|
||||
|
||||
// 状态变量
|
||||
private int ticksUntilNextAttack = 0;
|
||||
private int attackTicksRemaining = 0;
|
||||
private int warmupTicksRemaining = 0;
|
||||
private bool isAttacking = false;
|
||||
private bool isWarmingUp = false;
|
||||
private IntVec3 currentTarget;
|
||||
private Effecter warmupEffecter;
|
||||
private Effecter attackEffecter;
|
||||
|
||||
// 目标跟踪
|
||||
private List<IntVec3> previousTargets = new List<IntVec3>();
|
||||
|
||||
public override void Initialize(CompProperties props)
|
||||
{
|
||||
base.Initialize(props);
|
||||
|
||||
ticksUntilNextAttack = Props.ticksBetweenAttacks;
|
||||
|
||||
Log.Message($"Ship Artillery initialized: {Props.ticksBetweenAttacks} ticks between attacks, {Props.attackRadius} radius");
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
if (parent is not FlyOver flyOver || !flyOver.Spawned || flyOver.Map == null)
|
||||
return;
|
||||
|
||||
// 更新预热状态
|
||||
if (isWarmingUp)
|
||||
{
|
||||
UpdateWarmup(flyOver);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新攻击状态
|
||||
if (isAttacking)
|
||||
{
|
||||
UpdateAttack(flyOver);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否开始攻击
|
||||
if (ticksUntilNextAttack <= 0)
|
||||
{
|
||||
StartAttack(flyOver);
|
||||
}
|
||||
else
|
||||
{
|
||||
ticksUntilNextAttack--;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartAttack(FlyOver flyOver)
|
||||
{
|
||||
if (!CanAttack(flyOver))
|
||||
return;
|
||||
|
||||
// 选择目标区域
|
||||
currentTarget = SelectTarget(flyOver);
|
||||
|
||||
if (!currentTarget.IsValid || !currentTarget.InBounds(flyOver.Map))
|
||||
{
|
||||
Log.Warning("Ship Artillery: Invalid target selected, skipping attack");
|
||||
ticksUntilNextAttack = Props.ticksBetweenAttacks;
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Message($"Ship Artillery starting attack on target area: {currentTarget} (attack radius: {Props.attackRadius})");
|
||||
|
||||
// 开始预热
|
||||
isWarmingUp = true;
|
||||
warmupTicksRemaining = Props.warmupTicks;
|
||||
|
||||
// 启动预热效果
|
||||
if (Props.warmupEffect != null)
|
||||
{
|
||||
warmupEffecter = Props.warmupEffect.Spawn();
|
||||
warmupEffecter.Trigger(new TargetInfo(currentTarget, flyOver.Map), new TargetInfo(currentTarget, flyOver.Map));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateWarmup(FlyOver flyOver)
|
||||
{
|
||||
warmupTicksRemaining--;
|
||||
|
||||
// 维持预热效果
|
||||
if (warmupEffecter != null)
|
||||
{
|
||||
warmupEffecter.EffectTick(new TargetInfo(currentTarget, flyOver.Map), new TargetInfo(currentTarget, flyOver.Map));
|
||||
}
|
||||
|
||||
// 生成预热粒子
|
||||
if (Props.warmupFleck != null && Rand.MTBEventOccurs(0.1f, 1f, 1f))
|
||||
{
|
||||
FleckMaker.Static(currentTarget.ToVector3Shifted(), flyOver.Map, Props.warmupFleck);
|
||||
}
|
||||
|
||||
// 预热完成,开始攻击
|
||||
if (warmupTicksRemaining <= 0)
|
||||
{
|
||||
StartFiring(flyOver);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartFiring(FlyOver flyOver)
|
||||
{
|
||||
isWarmingUp = false;
|
||||
isAttacking = true;
|
||||
attackTicksRemaining = Props.attackDurationTicks;
|
||||
|
||||
// 清理预热效果
|
||||
warmupEffecter?.Cleanup();
|
||||
warmupEffecter = null;
|
||||
|
||||
// 启动攻击效果
|
||||
if (Props.attackEffect != null)
|
||||
{
|
||||
attackEffecter = Props.attackEffect.Spawn();
|
||||
}
|
||||
|
||||
Log.Message($"Ship Artillery started firing at area {currentTarget}");
|
||||
|
||||
// 发送攻击通知
|
||||
if (Props.sendAttackLetter)
|
||||
{
|
||||
SendAttackLetter(flyOver);
|
||||
}
|
||||
|
||||
// 立即执行第一轮齐射
|
||||
ExecuteVolley(flyOver);
|
||||
}
|
||||
|
||||
private void UpdateAttack(FlyOver flyOver)
|
||||
{
|
||||
attackTicksRemaining--;
|
||||
|
||||
// 维持攻击效果
|
||||
if (attackEffecter != null)
|
||||
{
|
||||
attackEffecter.EffectTick(new TargetInfo(currentTarget, flyOver.Map), new TargetInfo(currentTarget, flyOver.Map));
|
||||
}
|
||||
|
||||
// 在攻击期间定期发射炮弹
|
||||
if (attackTicksRemaining % 60 == 0) // 每秒发射一次
|
||||
{
|
||||
ExecuteVolley(flyOver);
|
||||
}
|
||||
|
||||
// 生成攻击粒子
|
||||
if (Props.attackFleck != null && Rand.MTBEventOccurs(0.2f, 1f, 1f))
|
||||
{
|
||||
Vector3 randomOffset = new Vector3(Rand.Range(-3f, 3f), 0f, Rand.Range(-3f, 3f));
|
||||
FleckMaker.Static((currentTarget.ToVector3Shifted() + randomOffset), flyOver.Map, Props.attackFleck);
|
||||
}
|
||||
|
||||
// 攻击结束
|
||||
if (attackTicksRemaining <= 0)
|
||||
{
|
||||
EndAttack(flyOver);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteVolley(FlyOver flyOver)
|
||||
{
|
||||
for (int i = 0; i < Props.shellsPerVolley; i++)
|
||||
{
|
||||
FireShell(flyOver);
|
||||
}
|
||||
}
|
||||
|
||||
private void FireShell(FlyOver flyOver)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 选择炮弹类型
|
||||
ThingDef shellDef = SelectShellDef();
|
||||
if (shellDef == null)
|
||||
{
|
||||
Log.Error("Ship Artillery: No valid shell def found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接选择随机目标
|
||||
IntVec3 shellTarget = SelectRandomTarget(flyOver);
|
||||
|
||||
// 关键修复:使用 SkyfallerMaker 创建并立即生成 Skyfaller
|
||||
SkyfallerMaker.SpawnSkyfaller(shellDef, shellTarget, flyOver.Map);
|
||||
|
||||
float distanceFromCenter = shellTarget.DistanceTo(currentTarget);
|
||||
Log.Message($"Ship Artillery fired shell at {shellTarget} (distance from center: {distanceFromCenter:F1})");
|
||||
|
||||
// 播放音效
|
||||
if (Props.attackSound != null)
|
||||
{
|
||||
Props.attackSound.PlayOneShot(new TargetInfo(shellTarget, flyOver.Map));
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"Error firing ship artillery shell: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private ThingDef SelectShellDef()
|
||||
{
|
||||
if (Props.skyfallerDefs != null && Props.skyfallerDefs.Count > 0)
|
||||
{
|
||||
if (Props.useDifferentShells)
|
||||
{
|
||||
return Props.skyfallerDefs.RandomElement();
|
||||
}
|
||||
else
|
||||
{
|
||||
return Props.skyfallerDefs[0];
|
||||
}
|
||||
}
|
||||
|
||||
return Props.skyfallerDef;
|
||||
}
|
||||
|
||||
private IntVec3 GetLaunchPosition(FlyOver flyOver)
|
||||
{
|
||||
// 从飞越物体的位置发射
|
||||
IntVec3 launchPos = flyOver.Position;
|
||||
|
||||
// 确保发射位置在地图边界内
|
||||
if (!launchPos.InBounds(flyOver.Map))
|
||||
{
|
||||
launchPos = flyOver.Map.Center;
|
||||
}
|
||||
|
||||
return launchPos;
|
||||
}
|
||||
|
||||
// 简化的目标选择 - 每次直接随机选择目标
|
||||
private IntVec3 SelectRandomTarget(FlyOver flyOver)
|
||||
{
|
||||
IntVec3 center = GetFlyOverPosition(flyOver) + Props.targetOffset;
|
||||
return FindRandomTargetInRadius(center, flyOver.Map, Props.attackRadius);
|
||||
}
|
||||
|
||||
private IntVec3 SelectTarget(FlyOver flyOver)
|
||||
{
|
||||
// 获取飞越物体当前位置作为基础中心
|
||||
IntVec3 flyOverPos = GetFlyOverPosition(flyOver);
|
||||
IntVec3 center = flyOverPos + Props.targetOffset;
|
||||
|
||||
Log.Message($"FlyOver position: {flyOverPos}, Center for targeting: {center}");
|
||||
|
||||
// 在攻击半径内选择随机目标
|
||||
return FindRandomTargetInRadius(center, flyOver.Map, Props.attackRadius);
|
||||
}
|
||||
|
||||
// 改进的飞越物体位置获取
|
||||
private IntVec3 GetFlyOverPosition(FlyOver flyOver)
|
||||
{
|
||||
// 优先使用 DrawPos,因为它反映实际视觉位置
|
||||
Vector3 drawPos = flyOver.DrawPos;
|
||||
IntVec3 result = new IntVec3(
|
||||
Mathf.RoundToInt(drawPos.x),
|
||||
0,
|
||||
Mathf.RoundToInt(drawPos.z)
|
||||
);
|
||||
|
||||
// 如果 DrawPos 无效,回退到 Position
|
||||
if (!result.InBounds(flyOver.Map))
|
||||
{
|
||||
result = flyOver.Position;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 目标查找逻辑 - 基于攻击半径
|
||||
private IntVec3 FindRandomTargetInRadius(IntVec3 center, Map map, float radius)
|
||||
{
|
||||
Log.Message($"Finding target around {center} with radius {radius}");
|
||||
|
||||
// 如果半径为0,直接返回中心
|
||||
if (radius <= 0)
|
||||
return center;
|
||||
|
||||
bool ignoreProtectionForThisTarget = Rand.Value < Props.ignoreProtectionChance;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// 在圆形区域内随机选择
|
||||
float angle = Rand.Range(0f, 360f);
|
||||
float distance = Rand.Range(0f, radius);
|
||||
|
||||
IntVec3 potentialTarget = center;
|
||||
potentialTarget.x += Mathf.RoundToInt(Mathf.Cos(angle * Mathf.Deg2Rad) * distance);
|
||||
potentialTarget.z += Mathf.RoundToInt(Mathf.Sin(angle * Mathf.Deg2Rad) * distance);
|
||||
|
||||
if (potentialTarget.InBounds(map) && IsValidTarget(potentialTarget, map, ignoreProtectionForThisTarget))
|
||||
{
|
||||
// 避免重复攻击同一位置
|
||||
if (!previousTargets.Contains(potentialTarget) || previousTargets.Count > 10)
|
||||
{
|
||||
if (previousTargets.Count > 10)
|
||||
previousTargets.RemoveAt(0);
|
||||
|
||||
previousTargets.Add(potentialTarget);
|
||||
|
||||
float actualDistance = potentialTarget.DistanceTo(center);
|
||||
Log.Message($"Found valid target at {potentialTarget} (distance from center: {actualDistance:F1})");
|
||||
|
||||
if (ignoreProtectionForThisTarget)
|
||||
{
|
||||
Log.Warning($"Protection ignored for target selection! May target player assets.");
|
||||
}
|
||||
|
||||
return potentialTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:使用地图随机位置
|
||||
Log.Warning("Could not find valid target in radius, using fallback");
|
||||
CellRect mapRect = CellRect.WholeMap(map);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
IntVec3 fallbackTarget = mapRect.RandomCell;
|
||||
if (IsValidTarget(fallbackTarget, map, ignoreProtectionForThisTarget))
|
||||
{
|
||||
return fallbackTarget;
|
||||
}
|
||||
}
|
||||
|
||||
// 最终回退:使用中心
|
||||
return center;
|
||||
}
|
||||
|
||||
// 检查是否靠近玩家资产
|
||||
private bool IsNearPlayerAssets(IntVec3 cell, Map map)
|
||||
{
|
||||
if (!Props.avoidPlayerAssets)
|
||||
return false;
|
||||
|
||||
foreach (IntVec3 checkCell in GenRadial.RadialCellsAround(cell, Props.playerAssetAvoidanceRadius, true))
|
||||
{
|
||||
if (!checkCell.InBounds(map))
|
||||
continue;
|
||||
|
||||
// 检查玩家建筑
|
||||
var building = checkCell.GetEdifice(map);
|
||||
if (building != null && building.Faction == Faction.OfPlayer)
|
||||
return true;
|
||||
|
||||
// 检查玩家殖民者
|
||||
var pawn = map.thingGrid.ThingAt<Pawn>(checkCell);
|
||||
if (pawn != null && pawn.Faction == Faction.OfPlayer && pawn.RaceProps.Humanlike)
|
||||
return true;
|
||||
|
||||
// 检查玩家动物
|
||||
var animal = map.thingGrid.ThingAt<Pawn>(checkCell);
|
||||
if (animal != null && animal.Faction == Faction.OfPlayer && animal.RaceProps.Animal)
|
||||
return true;
|
||||
|
||||
// 检查玩家物品
|
||||
var items = checkCell.GetThingList(map);
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Faction == Faction.OfPlayer && item.def.category == ThingCategory.Item)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsValidTarget(IntVec3 target, Map map, bool ignoreProtection = false)
|
||||
{
|
||||
if (!target.InBounds(map))
|
||||
return false;
|
||||
|
||||
// 避开玩家资产(除非无视保护机制)
|
||||
if (Props.avoidPlayerAssets && !ignoreProtection && IsNearPlayerAssets(target, map))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 避免击中飞越物体本身
|
||||
if (Props.avoidHittingFlyOver)
|
||||
{
|
||||
float distanceToFlyOver = target.DistanceTo(parent.Position);
|
||||
if (distanceToFlyOver < 10f) // 增加安全距离
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CanAttack(FlyOver flyOver)
|
||||
{
|
||||
if (flyOver.Map == null)
|
||||
return false;
|
||||
|
||||
if (flyOver.hasCompleted)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EndAttack(FlyOver flyOver)
|
||||
{
|
||||
isAttacking = false;
|
||||
|
||||
// 清理效果
|
||||
attackEffecter?.Cleanup();
|
||||
attackEffecter = null;
|
||||
|
||||
// 重置计时器
|
||||
if (Props.continuousAttack && !flyOver.hasCompleted)
|
||||
{
|
||||
ticksUntilNextAttack = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
ticksUntilNextAttack = Props.ticksBetweenAttacks;
|
||||
}
|
||||
|
||||
Log.Message($"Ship Artillery attack ended");
|
||||
}
|
||||
|
||||
private void SendAttackLetter(FlyOver flyOver)
|
||||
{
|
||||
try
|
||||
{
|
||||
string label = Props.customLetterLabel ?? "ShipArtilleryAttack".Translate();
|
||||
string text = Props.customLetterText ?? "ShipArtilleryAttackDesc".Translate();
|
||||
|
||||
Find.LetterStack.ReceiveLetter(
|
||||
label,
|
||||
text,
|
||||
Props.letterDef,
|
||||
new TargetInfo(currentTarget, flyOver.Map)
|
||||
);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"Error sending ship artillery letter: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref ticksUntilNextAttack, "ticksUntilNextAttack", 0);
|
||||
Scribe_Values.Look(ref attackTicksRemaining, "attackTicksRemaining", 0);
|
||||
Scribe_Values.Look(ref warmupTicksRemaining, "warmupTicksRemaining", 0);
|
||||
Scribe_Values.Look(ref isAttacking, "isAttacking", false);
|
||||
Scribe_Values.Look(ref isWarmingUp, "isWarmingUp", false);
|
||||
Scribe_Values.Look(ref currentTarget, "currentTarget");
|
||||
Scribe_Collections.Look(ref previousTargets, "previousTargets", LookMode.Value);
|
||||
}
|
||||
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
if (DebugSettings.ShowDevGizmos && parent is FlyOver)
|
||||
{
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "Dev: Trigger Artillery Attack",
|
||||
action = () => StartAttack(parent as FlyOver)
|
||||
};
|
||||
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "Dev: Fire Single Shell",
|
||||
action = () => FireShell(parent as FlyOver)
|
||||
};
|
||||
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = $"Dev: Status - Next: {ticksUntilNextAttack}, Attacking: {isAttacking}",
|
||||
action = () => {}
|
||||
};
|
||||
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = $"Dev: Debug Position Info",
|
||||
action = () =>
|
||||
{
|
||||
if (parent is FlyOver flyOver)
|
||||
{
|
||||
IntVec3 flyOverPos = GetFlyOverPosition(flyOver);
|
||||
Log.Message($"FlyOver - DrawPos: {flyOver.DrawPos}, Position: {flyOver.Position}, Calculated: {flyOverPos}");
|
||||
Log.Message($"Current Target: {currentTarget}, Distance: {flyOverPos.DistanceTo(currentTarget):F1}");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerAttack()
|
||||
{
|
||||
if (parent is FlyOver flyOver)
|
||||
{
|
||||
StartAttack(flyOver);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTarget(IntVec3 target)
|
||||
{
|
||||
currentTarget = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,926 @@
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompAbilityEffect_SpawnFlyOver : CompAbilityEffect
|
||||
{
|
||||
public new CompProperties_AbilitySpawnFlyOver Props => (CompProperties_AbilitySpawnFlyOver)props;
|
||||
|
||||
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
|
||||
{
|
||||
base.Apply(target, dest);
|
||||
|
||||
if (parent.pawn == null || parent.pawn.Map == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Log.Message($"FlyOver skill activated by {parent.pawn.Label} at position {parent.pawn.Position}");
|
||||
Log.Message($"Target cell: {target.Cell}, Dest: {dest.Cell}");
|
||||
|
||||
// 计算起始和结束位置
|
||||
IntVec3 startPos, endPos;
|
||||
|
||||
// 根据进场类型选择不同的计算方法
|
||||
if (Props.approachType == ApproachType.Perpendicular)
|
||||
{
|
||||
CalculatePerpendicularPath(target, out startPos, out endPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
startPos = CalculateStartPosition(target);
|
||||
endPos = CalculateEndPosition(target, startPos);
|
||||
}
|
||||
|
||||
// 确保位置安全
|
||||
startPos = GetSafeMapPosition(startPos, parent.pawn.Map);
|
||||
endPos = GetSafeMapPosition(endPos, parent.pawn.Map);
|
||||
|
||||
Log.Message($"Final positions - Start: {startPos}, End: {endPos}");
|
||||
|
||||
// 验证位置是否有效
|
||||
if (!startPos.InBounds(parent.pawn.Map))
|
||||
{
|
||||
Log.Warning($"Start position {startPos} is out of bounds, adjusting to map center");
|
||||
startPos = parent.pawn.Map.Center;
|
||||
}
|
||||
|
||||
if (!endPos.InBounds(parent.pawn.Map))
|
||||
{
|
||||
Log.Warning($"End position {endPos} is out of bounds, adjusting to map center");
|
||||
endPos = parent.pawn.Map.Center;
|
||||
}
|
||||
|
||||
// 确保起点和终点不同
|
||||
if (startPos == endPos)
|
||||
{
|
||||
Log.Warning($"FlyOver start and end positions are the same: {startPos}. Adjusting end position.");
|
||||
IntVec3 randomOffset = new IntVec3(Rand.Range(-10, 11), 0, Rand.Range(-10, 11));
|
||||
endPos += randomOffset;
|
||||
endPos = GetSafeMapPosition(endPos, parent.pawn.Map);
|
||||
}
|
||||
|
||||
// 根据类型创建不同的飞越物体
|
||||
switch (Props.flyOverType)
|
||||
{
|
||||
case FlyOverType.Standard:
|
||||
default:
|
||||
CreateStandardFlyOver(startPos, endPos);
|
||||
break;
|
||||
case FlyOverType.GroundStrafing:
|
||||
CreateGroundStrafingFlyOver(startPos, endPos, target.Cell);
|
||||
break;
|
||||
case FlyOverType.SectorSurveillance:
|
||||
CreateSectorSurveillanceFlyOver(startPos, endPos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"Error spawning fly over: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// 修复的预览绘制方法
|
||||
public override void DrawEffectPreview(LocalTargetInfo target)
|
||||
{
|
||||
base.DrawEffectPreview(target);
|
||||
|
||||
if (parent.pawn != null && parent.pawn.Map != null)
|
||||
{
|
||||
Map map = parent.pawn.Map;
|
||||
|
||||
try
|
||||
{
|
||||
// 计算飞行路径
|
||||
IntVec3 startPos, endPos;
|
||||
if (Props.approachType == ApproachType.Perpendicular)
|
||||
{
|
||||
CalculatePerpendicularPath(target, out startPos, out endPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
startPos = CalculateStartPosition(target);
|
||||
endPos = CalculateEndPosition(target, startPos);
|
||||
}
|
||||
|
||||
// 确保位置在地图范围内
|
||||
startPos = GetSafeMapPosition(startPos, map);
|
||||
endPos = GetSafeMapPosition(endPos, map);
|
||||
|
||||
// 检查预览稳定性
|
||||
if (!IsPreviewStable(startPos, endPos, map))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据不同类型显示不同的预览
|
||||
if (Props.enableGroundStrafing && Props.showStrafePreview)
|
||||
{
|
||||
DrawStrafingAreaPreview(startPos, endPos, target.Cell);
|
||||
}
|
||||
else if (Props.enableSectorSurveillance && Props.showSectorPreview)
|
||||
{
|
||||
DrawSectorAreaPreview(startPos, endPos);
|
||||
}
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
// 忽略预览绘制中的错误,避免影响游戏体验
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 安全的位置计算方法
|
||||
private IntVec3 GetSafeMapPosition(IntVec3 pos, Map map)
|
||||
{
|
||||
if (map == null) return pos;
|
||||
|
||||
// 确保位置在地图范围内
|
||||
pos.x = Mathf.Clamp(pos.x, 0, map.Size.x - 1);
|
||||
pos.z = Mathf.Clamp(pos.z, 0, map.Size.z - 1);
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
// 预览绘制稳定性检查
|
||||
private bool IsPreviewStable(IntVec3 startPos, IntVec3 endPos, Map map)
|
||||
{
|
||||
if (map == null) return false;
|
||||
|
||||
// 检查位置是否有效
|
||||
if (!startPos.IsValid || !endPos.IsValid) return false;
|
||||
|
||||
// 检查位置是否在地图范围内
|
||||
if (!startPos.InBounds(map) || !endPos.InBounds(map)) return false;
|
||||
|
||||
// 检查距离是否合理(避免过短的路径)
|
||||
float distance = Vector3.Distance(startPos.ToVector3(), endPos.ToVector3());
|
||||
if (distance < 5f) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 修复:绘制地面扫射预览,现在接受目标单元格参数
|
||||
private void DrawStrafingAreaPreview(IntVec3 startPos, IntVec3 endPos, IntVec3 targetCell)
|
||||
{
|
||||
Map map = parent.pawn.Map;
|
||||
|
||||
// 计算飞行方向
|
||||
Vector3 flightDirection = (endPos.ToVector3() - startPos.ToVector3()).normalized;
|
||||
if (flightDirection == Vector3.zero)
|
||||
{
|
||||
flightDirection = Vector3.forward;
|
||||
}
|
||||
|
||||
// 只计算扫射影响区域的单元格
|
||||
List<IntVec3> strafeImpactCells = CalculateStrafingImpactCells(targetCell, flightDirection);
|
||||
|
||||
// 绘制扫射影响区域的预览单元格
|
||||
foreach (IntVec3 cell in strafeImpactCells)
|
||||
{
|
||||
if (cell.InBounds(map))
|
||||
{
|
||||
GenDraw.DrawFieldEdges(new List<IntVec3> { cell }, Props.strafePreviewColor, 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制飞行路径线
|
||||
GenDraw.DrawLineBetween(startPos.ToVector3Shifted(), endPos.ToVector3Shifted(), SimpleColor.Red, 0.2f);
|
||||
|
||||
// 绘制扫射范围边界
|
||||
DrawStrafingBoundaries(targetCell, flightDirection);
|
||||
}
|
||||
|
||||
// 修复:计算扫射影响区域的单元格,现在以目标单元格为中心
|
||||
private List<IntVec3> CalculateStrafingImpactCells(IntVec3 targetCell, Vector3 flightDirection)
|
||||
{
|
||||
List<IntVec3> cells = new List<IntVec3>();
|
||||
Map map = parent.pawn.Map;
|
||||
|
||||
// 计算垂直于飞行方向的方向
|
||||
Vector3 perpendicular = new Vector3(-flightDirection.z, 0f, flightDirection.x).normalized;
|
||||
|
||||
// 修复:以目标单元格为中心计算扫射区域
|
||||
Vector3 targetCenter = targetCell.ToVector3();
|
||||
|
||||
// 计算扫射区域的起始和结束位置(基于扫射长度,以目标为中心)
|
||||
float strafeHalfLength = Props.strafeLength * 0.5f;
|
||||
Vector3 strafeStart = targetCenter - flightDirection * strafeHalfLength;
|
||||
Vector3 strafeEnd = targetCenter + flightDirection * strafeHalfLength;
|
||||
|
||||
// 使用整数步进避免浮点精度问题
|
||||
int steps = Mathf.Max(1, Mathf.CeilToInt(Props.strafeLength));
|
||||
for (int i = 0; i <= steps; i++)
|
||||
{
|
||||
float progress = (float)i / steps;
|
||||
Vector3 centerPoint = Vector3.Lerp(strafeStart, strafeEnd, progress);
|
||||
|
||||
// 在垂直方向扩展扫射宽度
|
||||
for (int w = -Props.strafeWidth; w <= Props.strafeWidth; w++)
|
||||
{
|
||||
Vector3 offset = perpendicular * w;
|
||||
Vector3 cellPos = centerPoint + offset;
|
||||
|
||||
// 使用更精确的单元格转换
|
||||
IntVec3 cell = new IntVec3(
|
||||
Mathf.RoundToInt(cellPos.x),
|
||||
Mathf.RoundToInt(cellPos.y),
|
||||
Mathf.RoundToInt(cellPos.z)
|
||||
);
|
||||
|
||||
if (cell.InBounds(map) && !cells.Contains(cell))
|
||||
{
|
||||
cells.Add(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message($"Strafing Area: Calculated {cells.Count} impact cells centered at {targetCell}");
|
||||
return cells;
|
||||
}
|
||||
|
||||
// 修复:绘制扫射范围边界,现在以目标单元格为中心
|
||||
private void DrawStrafingBoundaries(IntVec3 targetCell, Vector3 flightDirection)
|
||||
{
|
||||
Map map = parent.pawn.Map;
|
||||
Vector3 perpendicular = new Vector3(-flightDirection.z, 0f, flightDirection.x).normalized;
|
||||
|
||||
// 修复:以目标单元格为中心
|
||||
Vector3 targetCenter = targetCell.ToVector3();
|
||||
|
||||
// 计算扫射区域的起始和结束位置
|
||||
float strafeHalfLength = Props.strafeLength * 0.5f;
|
||||
Vector3 strafeStart = targetCenter - flightDirection * strafeHalfLength;
|
||||
Vector3 strafeEnd = targetCenter + flightDirection * strafeHalfLength;
|
||||
|
||||
// 计算扫射区域的四个角
|
||||
Vector3 startLeft = strafeStart + perpendicular * Props.strafeWidth;
|
||||
Vector3 startRight = strafeStart - perpendicular * Props.strafeWidth;
|
||||
Vector3 endLeft = strafeEnd + perpendicular * Props.strafeWidth;
|
||||
Vector3 endRight = strafeEnd - perpendicular * Props.strafeWidth;
|
||||
|
||||
// 转换为 IntVec3 并确保在地图范围内
|
||||
IntVec3 startLeftCell = GetSafeMapPosition(new IntVec3((int)startLeft.x, (int)startLeft.y, (int)startLeft.z), map);
|
||||
IntVec3 startRightCell = GetSafeMapPosition(new IntVec3((int)startRight.x, (int)startRight.y, (int)startRight.z), map);
|
||||
IntVec3 endLeftCell = GetSafeMapPosition(new IntVec3((int)endLeft.x, (int)endLeft.y, (int)endLeft.z), map);
|
||||
IntVec3 endRightCell = GetSafeMapPosition(new IntVec3((int)endRight.x, (int)endRight.y, (int)endRight.z), map);
|
||||
|
||||
// 绘制边界线 - 只绘制在地图范围内的线段
|
||||
if (startLeftCell.InBounds(map) && endLeftCell.InBounds(map))
|
||||
GenDraw.DrawLineBetween(startLeftCell.ToVector3Shifted(), endLeftCell.ToVector3Shifted(), SimpleColor.Red, 0.2f);
|
||||
|
||||
if (startRightCell.InBounds(map) && endRightCell.InBounds(map))
|
||||
GenDraw.DrawLineBetween(startRightCell.ToVector3Shifted(), endRightCell.ToVector3Shifted(), SimpleColor.Red, 0.2f);
|
||||
|
||||
if (startLeftCell.InBounds(map) && startRightCell.InBounds(map))
|
||||
GenDraw.DrawLineBetween(startLeftCell.ToVector3Shifted(), startRightCell.ToVector3Shifted(), SimpleColor.Red, 0.2f);
|
||||
|
||||
if (endLeftCell.InBounds(map) && endRightCell.InBounds(map))
|
||||
GenDraw.DrawLineBetween(endLeftCell.ToVector3Shifted(), endRightCell.ToVector3Shifted(), SimpleColor.Red, 0.2f);
|
||||
}
|
||||
|
||||
// 绘制扇形区域预览
|
||||
private void DrawSectorAreaPreview(IntVec3 startPos, IntVec3 endPos)
|
||||
{
|
||||
Map map = parent.pawn.Map;
|
||||
|
||||
// 计算飞行方向
|
||||
Vector3 flightDirection = (endPos.ToVector3() - startPos.ToVector3()).normalized;
|
||||
if (flightDirection == Vector3.zero)
|
||||
{
|
||||
flightDirection = Vector3.forward;
|
||||
}
|
||||
|
||||
// 计算垂直于飞行方向的方向
|
||||
Vector3 perpendicular = new Vector3(-flightDirection.z, 0f, flightDirection.x).normalized;
|
||||
|
||||
// 使用strafeWidth来近似扇形扫过的区域宽度
|
||||
List<IntVec3> previewCells = CalculateRectangularPreviewArea(startPos, endPos, flightDirection, perpendicular);
|
||||
|
||||
// 绘制预览区域
|
||||
foreach (IntVec3 cell in previewCells)
|
||||
{
|
||||
if (cell.InBounds(map))
|
||||
{
|
||||
GenDraw.DrawFieldEdges(new List<IntVec3> { cell }, Props.sectorPreviewColor, 0.3f);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制飞行路径线
|
||||
GenDraw.DrawLineBetween(startPos.ToVector3Shifted(), endPos.ToVector3Shifted(), SimpleColor.Blue, 0.2f);
|
||||
|
||||
// 绘制预览区域边界
|
||||
DrawRectangularPreviewBoundaries(startPos, endPos, flightDirection, perpendicular);
|
||||
}
|
||||
|
||||
// 计算矩形预览区域(近似扇形扫过的区域)
|
||||
private List<IntVec3> CalculateRectangularPreviewArea(IntVec3 startPos, IntVec3 endPos, Vector3 flightDirection, Vector3 perpendicular)
|
||||
{
|
||||
List<IntVec3> cells = new List<IntVec3>();
|
||||
Map map = parent.pawn.Map;
|
||||
|
||||
// 计算飞行路径的总长度
|
||||
float totalPathLength = Vector3.Distance(startPos.ToVector3(), endPos.ToVector3());
|
||||
|
||||
// 沿着飞行路径计算预览单元格
|
||||
int steps = Mathf.Max(1, Mathf.CeilToInt(totalPathLength));
|
||||
for (int i = 0; i <= steps; i++)
|
||||
{
|
||||
float progress = (float)i / steps;
|
||||
Vector3 centerPoint = Vector3.Lerp(startPos.ToVector3(), endPos.ToVector3(), progress);
|
||||
|
||||
// 在垂直方向扩展预览宽度(使用strafeWidth)
|
||||
for (int w = -Props.strafeWidth; w <= Props.strafeWidth; w++)
|
||||
{
|
||||
Vector3 offset = perpendicular * w;
|
||||
Vector3 cellPos = centerPoint + offset;
|
||||
|
||||
// 使用精确的单元格转换
|
||||
IntVec3 cell = new IntVec3(
|
||||
Mathf.RoundToInt(cellPos.x),
|
||||
Mathf.RoundToInt(cellPos.y),
|
||||
Mathf.RoundToInt(cellPos.z)
|
||||
);
|
||||
|
||||
if (cell.InBounds(map) && !cells.Contains(cell))
|
||||
{
|
||||
cells.Add(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
// 绘制矩形预览边界
|
||||
private void DrawRectangularPreviewBoundaries(IntVec3 startPos, IntVec3 endPos, Vector3 flightDirection, Vector3 perpendicular)
|
||||
{
|
||||
Map map = parent.pawn.Map;
|
||||
|
||||
// 计算预览区域的四个角
|
||||
Vector3 startLeft = startPos.ToVector3() + perpendicular * Props.strafeWidth;
|
||||
Vector3 startRight = startPos.ToVector3() - perpendicular * Props.strafeWidth;
|
||||
Vector3 endLeft = endPos.ToVector3() + perpendicular * Props.strafeWidth;
|
||||
Vector3 endRight = endPos.ToVector3() - perpendicular * Props.strafeWidth;
|
||||
|
||||
// 转换为 IntVec3 并确保在地图范围内
|
||||
IntVec3 startLeftCell = GetSafeMapPosition(new IntVec3((int)startLeft.x, (int)startLeft.y, (int)startLeft.z), map);
|
||||
IntVec3 startRightCell = GetSafeMapPosition(new IntVec3((int)startRight.x, (int)startRight.y, (int)startRight.z), map);
|
||||
IntVec3 endLeftCell = GetSafeMapPosition(new IntVec3((int)endLeft.x, (int)endLeft.y, (int)endLeft.z), map);
|
||||
IntVec3 endRightCell = GetSafeMapPosition(new IntVec3((int)endRight.x, (int)endRight.y, (int)endRight.z), map);
|
||||
|
||||
// 绘制边界线 - 只绘制在地图范围内的线段
|
||||
if (startLeftCell.InBounds(map) && endLeftCell.InBounds(map))
|
||||
GenDraw.DrawLineBetween(startLeftCell.ToVector3Shifted(), endLeftCell.ToVector3Shifted(), SimpleColor.Blue, 0.2f);
|
||||
|
||||
if (startRightCell.InBounds(map) && endRightCell.InBounds(map))
|
||||
GenDraw.DrawLineBetween(startRightCell.ToVector3Shifted(), endRightCell.ToVector3Shifted(), SimpleColor.Blue, 0.2f);
|
||||
|
||||
if (startLeftCell.InBounds(map) && startRightCell.InBounds(map))
|
||||
GenDraw.DrawLineBetween(startLeftCell.ToVector3Shifted(), startRightCell.ToVector3Shifted(), SimpleColor.Blue, 0.2f);
|
||||
|
||||
if (endLeftCell.InBounds(map) && endRightCell.InBounds(map))
|
||||
GenDraw.DrawLineBetween(endLeftCell.ToVector3Shifted(), endRightCell.ToVector3Shifted(), SimpleColor.Blue, 0.2f);
|
||||
}
|
||||
|
||||
// 预处理扫射目标单元格
|
||||
private List<IntVec3> PreprocessStrafingTargets(List<IntVec3> potentialTargets, float fireChance)
|
||||
{
|
||||
List<IntVec3> confirmedTargets = new List<IntVec3>();
|
||||
List<IntVec3> missedCells = new List<IntVec3>();
|
||||
foreach (IntVec3 cell in potentialTargets)
|
||||
{
|
||||
if (Rand.Value <= fireChance)
|
||||
{
|
||||
confirmedTargets.Add(cell);
|
||||
}
|
||||
else
|
||||
{
|
||||
missedCells.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
// 应用最小和最大射弹数限制
|
||||
if (Props.maxStrafeProjectiles > -1 && confirmedTargets.Count > Props.maxStrafeProjectiles)
|
||||
{
|
||||
// 如果超出最大值,随机丢弃一些目标
|
||||
confirmedTargets = confirmedTargets.InRandomOrder().Take(Props.maxStrafeProjectiles).ToList();
|
||||
}
|
||||
|
||||
if (Props.minStrafeProjectiles > -1 && confirmedTargets.Count < Props.minStrafeProjectiles)
|
||||
{
|
||||
// 如果不足最小值,从之前未选中的格子里补充
|
||||
int needed = Props.minStrafeProjectiles - confirmedTargets.Count;
|
||||
if (needed > 0 && missedCells.Count > 0)
|
||||
{
|
||||
confirmedTargets.AddRange(missedCells.InRandomOrder().Take(Mathf.Min(needed, missedCells.Count)));
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message($"Strafing Preprocess: {confirmedTargets.Count}/{potentialTargets.Count} cells confirmed after min/max adjustment.");
|
||||
return confirmedTargets;
|
||||
}
|
||||
|
||||
// 修复:创建地面扫射飞越,现在接受目标单元格参数
|
||||
private void CreateGroundStrafingFlyOver(IntVec3 startPos, IntVec3 endPos, IntVec3 targetCell)
|
||||
{
|
||||
ThingDef flyOverDef = Props.flyOverDef ?? DefDatabase<ThingDef>.GetNamedSilentFail("ARA_HiveCorvette");
|
||||
if (flyOverDef == null)
|
||||
{
|
||||
Log.Warning("No fly over def specified for ground strafing fly over");
|
||||
return;
|
||||
}
|
||||
|
||||
FlyOver flyOver = FlyOver.MakeFlyOver(
|
||||
flyOverDef,
|
||||
startPos,
|
||||
endPos,
|
||||
parent.pawn.Map,
|
||||
Props.flightSpeed,
|
||||
Props.altitude,
|
||||
casterPawn: parent.pawn
|
||||
);
|
||||
|
||||
// 设置基本属性
|
||||
flyOver.spawnContentsOnImpact = Props.dropContentsOnImpact;
|
||||
flyOver.playFlyOverSound = Props.playFlyOverSound;
|
||||
|
||||
// 获取扫射组件并设置预处理后的目标单元格
|
||||
CompGroundStrafing strafingComp = flyOver.GetComp<CompGroundStrafing>();
|
||||
if (strafingComp != null)
|
||||
{
|
||||
// 修复:计算扫射区域的所有单元格,以目标单元格为中心
|
||||
Vector3 flightDirection = (endPos.ToVector3() - startPos.ToVector3()).normalized;
|
||||
List<IntVec3> potentialTargetCells = CalculateStrafingImpactCells(targetCell, flightDirection);
|
||||
|
||||
if (potentialTargetCells.Count > 0)
|
||||
{
|
||||
// 预处理:根据概率筛选实际会被射击的单元格
|
||||
List<IntVec3> confirmedTargetCells = PreprocessStrafingTargets(
|
||||
potentialTargetCells,
|
||||
Props.strafeFireChance
|
||||
);
|
||||
|
||||
if (confirmedTargetCells.Count > 0)
|
||||
{
|
||||
strafingComp.SetConfirmedTargets(confirmedTargetCells);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("No confirmed target cells after preprocessing!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("No potential target cells calculated for ground strafing!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("FlyOver def does not have CompGroundStrafing component!");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建扇形监视飞越
|
||||
private void CreateSectorSurveillanceFlyOver(IntVec3 startPos, IntVec3 endPos)
|
||||
{
|
||||
ThingDef flyOverDef = Props.flyOverDef ?? DefDatabase<ThingDef>.GetNamedSilentFail("ARA_HiveCorvette");
|
||||
if (flyOverDef == null)
|
||||
{
|
||||
Log.Warning("No fly over def specified for sector surveillance fly over");
|
||||
return;
|
||||
}
|
||||
|
||||
FlyOver flyOver = FlyOver.MakeFlyOver(
|
||||
flyOverDef,
|
||||
startPos,
|
||||
endPos,
|
||||
parent.pawn.Map,
|
||||
Props.flightSpeed,
|
||||
Props.altitude,
|
||||
casterPawn: parent.pawn
|
||||
);
|
||||
|
||||
// 设置基本属性
|
||||
flyOver.spawnContentsOnImpact = Props.dropContentsOnImpact;
|
||||
flyOver.playFlyOverSound = Props.playFlyOverSound;
|
||||
|
||||
Log.Message($"SectorSurveillance FlyOver created: {flyOver} from {startPos} to {endPos}");
|
||||
}
|
||||
|
||||
// 计算垂直线进场路径
|
||||
private void CalculatePerpendicularPath(LocalTargetInfo target, out IntVec3 startPos, out IntVec3 endPos)
|
||||
{
|
||||
Map map = parent.pawn.Map;
|
||||
IntVec3 casterPos = parent.pawn.Position;
|
||||
IntVec3 targetPos = target.Cell;
|
||||
|
||||
Log.Message($"Calculating perpendicular path: Caster={casterPos}, Target={targetPos}");
|
||||
|
||||
// 计算施法者到目标的方向向量
|
||||
Vector3 directionToTarget = (targetPos.ToVector3() - casterPos.ToVector3()).normalized;
|
||||
|
||||
// 如果方向为零向量,使用随机方向
|
||||
if (directionToTarget == Vector3.zero)
|
||||
{
|
||||
directionToTarget = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
|
||||
Log.Message($"Using random direction: {directionToTarget}");
|
||||
}
|
||||
|
||||
// 计算垂直于施法者-目标连线的方向(旋转90度)
|
||||
Vector3 perpendicularDirection = new Vector3(-directionToTarget.z, 0, directionToTarget.x).normalized;
|
||||
|
||||
Log.Message($"Perpendicular direction: {perpendicularDirection}");
|
||||
|
||||
// 从目标点出发,向垂直方向的两侧延伸找到地图边缘
|
||||
IntVec3 edge1 = FindMapEdgeInDirection(map, targetPos, perpendicularDirection);
|
||||
IntVec3 edge2 = FindMapEdgeInDirection(map, targetPos, -perpendicularDirection);
|
||||
|
||||
// 随机选择起点和终点(确保目标点在路径上)
|
||||
if (Rand.Value < 0.5f)
|
||||
{
|
||||
startPos = edge1;
|
||||
endPos = edge2;
|
||||
}
|
||||
else
|
||||
{
|
||||
startPos = edge2;
|
||||
endPos = edge1;
|
||||
}
|
||||
|
||||
Log.Message($"Perpendicular path: {startPos} -> {targetPos} -> {endPos}");
|
||||
}
|
||||
|
||||
// 在指定方向上找到地图边缘
|
||||
private IntVec3 FindMapEdgeInDirection(Map map, IntVec3 fromPos, Vector3 direction)
|
||||
{
|
||||
// 确保方向向量有效
|
||||
if (direction == Vector3.zero)
|
||||
{
|
||||
direction = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
|
||||
}
|
||||
|
||||
// 使用更精确的地图边界计算
|
||||
IntVec3 mapCenter = map.Center;
|
||||
IntVec3 mapSize = new IntVec3(map.Size.x, 0, map.Size.z);
|
||||
|
||||
// 计算与地图边界的交点
|
||||
Vector3 fromVec = fromPos.ToVector3();
|
||||
Vector3 dirNormalized = direction.normalized;
|
||||
|
||||
// 计算到各个边界的距离
|
||||
float tMin = float.MaxValue;
|
||||
IntVec3? bestEdgePos = null;
|
||||
|
||||
// 检查四个边界
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
float t = 0f;
|
||||
IntVec3 edgePos = IntVec3.Invalid;
|
||||
|
||||
switch (i)
|
||||
{
|
||||
case 0: // 左边界 (x = 0)
|
||||
if (Mathf.Abs(dirNormalized.x) > 0.001f)
|
||||
{
|
||||
t = (0 - fromVec.x) / dirNormalized.x;
|
||||
if (t > 0)
|
||||
{
|
||||
float z = fromVec.z + dirNormalized.z * t;
|
||||
if (z >= 0 && z < map.Size.z)
|
||||
{
|
||||
edgePos = new IntVec3(0, 0, Mathf.RoundToInt(z));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: // 右边界 (x = map.Size.x - 1)
|
||||
if (Mathf.Abs(dirNormalized.x) > 0.001f)
|
||||
{
|
||||
t = (map.Size.x - 1 - fromVec.x) / dirNormalized.x;
|
||||
if (t > 0)
|
||||
{
|
||||
float z = fromVec.z + dirNormalized.z * t;
|
||||
if (z >= 0 && z < map.Size.z)
|
||||
{
|
||||
edgePos = new IntVec3(map.Size.x - 1, 0, Mathf.RoundToInt(z));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // 下边界 (z = 0)
|
||||
if (Mathf.Abs(dirNormalized.z) > 0.001f)
|
||||
{
|
||||
t = (0 - fromVec.z) / dirNormalized.z;
|
||||
if (t > 0)
|
||||
{
|
||||
float x = fromVec.x + dirNormalized.x * t;
|
||||
if (x >= 0 && x < map.Size.x)
|
||||
{
|
||||
edgePos = new IntVec3(Mathf.RoundToInt(x), 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // 上边界 (z = map.Size.z - 1)
|
||||
if (Mathf.Abs(dirNormalized.z) > 0.001f)
|
||||
{
|
||||
t = (map.Size.z - 1 - fromVec.z) / dirNormalized.z;
|
||||
if (t > 0)
|
||||
{
|
||||
float x = fromVec.x + dirNormalized.x * t;
|
||||
if (x >= 0 && x < map.Size.x)
|
||||
{
|
||||
edgePos = new IntVec3(Mathf.RoundToInt(x), 0, map.Size.z - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 找到最近的有效边界点
|
||||
if (edgePos.IsValid && edgePos.InBounds(map) && t > 0 && t < tMin)
|
||||
{
|
||||
tMin = t;
|
||||
bestEdgePos = edgePos;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestEdgePos.HasValue)
|
||||
{
|
||||
return bestEdgePos.Value;
|
||||
}
|
||||
|
||||
// 如果没找到合适的边界点,使用随机边缘位置
|
||||
Log.Warning($"Could not find map edge in direction {direction}, using random edge");
|
||||
return GetRandomMapEdgePosition(map);
|
||||
}
|
||||
|
||||
// 原有的位置计算方法
|
||||
private IntVec3 CalculateStartPosition(LocalTargetInfo target)
|
||||
{
|
||||
Map map = parent.pawn.Map;
|
||||
|
||||
switch (Props.startPosition)
|
||||
{
|
||||
case StartPosition.Caster:
|
||||
return parent.pawn.Position;
|
||||
|
||||
case StartPosition.MapEdge:
|
||||
return GetMapEdgePosition(map, GetDirectionFromCasterToTarget(target));
|
||||
|
||||
case StartPosition.CustomOffset:
|
||||
return GetSafeMapPosition(parent.pawn.Position + Props.customStartOffset, map);
|
||||
|
||||
case StartPosition.RandomMapEdge:
|
||||
return GetRandomMapEdgePosition(map);
|
||||
|
||||
default:
|
||||
return parent.pawn.Position;
|
||||
}
|
||||
}
|
||||
|
||||
private IntVec3 CalculateEndPosition(LocalTargetInfo target, IntVec3 startPos)
|
||||
{
|
||||
Map map = parent.pawn.Map;
|
||||
IntVec3 endPos;
|
||||
|
||||
switch (Props.endPosition)
|
||||
{
|
||||
case EndPosition.TargetCell:
|
||||
endPos = target.Cell;
|
||||
break;
|
||||
|
||||
case EndPosition.OppositeMapEdge:
|
||||
endPos = GetOppositeMapEdgeThroughCenter(map, startPos);
|
||||
break;
|
||||
|
||||
case EndPosition.CustomOffset:
|
||||
endPos = GetSafeMapPosition(target.Cell + Props.customEndOffset, map);
|
||||
break;
|
||||
|
||||
case EndPosition.FixedDistance:
|
||||
endPos = GetFixedDistancePosition(startPos, target.Cell);
|
||||
break;
|
||||
|
||||
case EndPosition.RandomMapEdge:
|
||||
endPos = GetRandomMapEdgePosition(map);
|
||||
Log.Message($"Random map edge selected as end position: {endPos}");
|
||||
break;
|
||||
|
||||
default:
|
||||
endPos = target.Cell;
|
||||
break;
|
||||
}
|
||||
|
||||
return GetSafeMapPosition(endPos, map);
|
||||
}
|
||||
|
||||
// 原有的辅助方法
|
||||
private IntVec3 GetOppositeMapEdgeThroughCenter(Map map, IntVec3 startPos)
|
||||
{
|
||||
IntVec3 center = map.Center;
|
||||
Vector3 toCenter = (center.ToVector3() - startPos.ToVector3()).normalized;
|
||||
|
||||
if (toCenter == Vector3.zero)
|
||||
{
|
||||
toCenter = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
|
||||
Log.Message($"Using random direction to center: {toCenter}");
|
||||
}
|
||||
|
||||
Vector3 fromCenter = toCenter;
|
||||
IntVec3 oppositeEdge = GetMapEdgePositionFromCenter(map, fromCenter);
|
||||
|
||||
Log.Message($"Found opposite edge through center: {oppositeEdge}");
|
||||
return oppositeEdge;
|
||||
}
|
||||
|
||||
private IntVec3 GetMapEdgePositionFromCenter(Map map, Vector3 direction)
|
||||
{
|
||||
IntVec3 center = map.Center;
|
||||
float maxDist = Mathf.Max(map.Size.x, map.Size.z) * 0.6f;
|
||||
|
||||
for (int i = 1; i <= maxDist; i++)
|
||||
{
|
||||
IntVec3 testPos = center + new IntVec3(
|
||||
Mathf.RoundToInt(direction.x * i),
|
||||
0,
|
||||
Mathf.RoundToInt(direction.z * i));
|
||||
|
||||
if (!testPos.InBounds(map))
|
||||
{
|
||||
IntVec3 edgePos = FindClosestValidPosition(testPos, map);
|
||||
Log.Message($"Found map edge from center: {edgePos} (direction: {direction}, distance: {i})");
|
||||
return edgePos;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Warning("Could not find map edge from center, using random edge");
|
||||
return GetRandomMapEdgePosition(map);
|
||||
}
|
||||
|
||||
private IntVec3 GetMapEdgePosition(Map map, Vector3 direction)
|
||||
{
|
||||
if (direction == Vector3.zero)
|
||||
{
|
||||
direction = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
|
||||
Log.Message($"Using random direction: {direction}");
|
||||
}
|
||||
|
||||
IntVec3 center = map.Center;
|
||||
float maxDist = Mathf.Max(map.Size.x, map.Size.z) * 0.6f;
|
||||
|
||||
for (int i = 1; i <= maxDist; i++)
|
||||
{
|
||||
IntVec3 testPos = center + new IntVec3(
|
||||
Mathf.RoundToInt(direction.x * i),
|
||||
0,
|
||||
Mathf.RoundToInt(direction.z * i));
|
||||
|
||||
if (!testPos.InBounds(map))
|
||||
{
|
||||
IntVec3 edgePos = FindClosestValidPosition(testPos, map);
|
||||
Log.Message($"Found map edge position: {edgePos} (direction: {direction}, distance: {i})");
|
||||
return edgePos;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Warning("Could not find map edge in direction, using random edge");
|
||||
return GetRandomMapEdgePosition(map);
|
||||
}
|
||||
|
||||
private IntVec3 FindClosestValidPosition(IntVec3 invalidPos, Map map)
|
||||
{
|
||||
for (int radius = 1; radius <= 5; radius++)
|
||||
{
|
||||
foreach (IntVec3 pos in GenRadial.RadialPatternInRadius(radius))
|
||||
{
|
||||
IntVec3 testPos = invalidPos + pos;
|
||||
if (testPos.InBounds(map))
|
||||
{
|
||||
return testPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map.Center;
|
||||
}
|
||||
|
||||
private IntVec3 GetRandomMapEdgePosition(Map map)
|
||||
{
|
||||
int edge = Rand.Range(0, 4);
|
||||
int x, z;
|
||||
|
||||
switch (edge)
|
||||
{
|
||||
case 0: // 上边
|
||||
x = Rand.Range(0, map.Size.x);
|
||||
z = 0;
|
||||
break;
|
||||
case 1: // 右边
|
||||
x = map.Size.x - 1;
|
||||
z = Rand.Range(0, map.Size.z);
|
||||
break;
|
||||
case 2: // 下边
|
||||
x = Rand.Range(0, map.Size.x);
|
||||
z = map.Size.z - 1;
|
||||
break;
|
||||
case 3: // 左边
|
||||
default:
|
||||
x = 0;
|
||||
z = Rand.Range(0, map.Size.z);
|
||||
break;
|
||||
}
|
||||
|
||||
IntVec3 edgePos = new IntVec3(x, 0, z);
|
||||
Log.Message($"Random map edge position: {edgePos}");
|
||||
return edgePos;
|
||||
}
|
||||
|
||||
private IntVec3 GetFixedDistancePosition(IntVec3 startPos, IntVec3 targetPos)
|
||||
{
|
||||
Vector3 direction = (targetPos.ToVector3() - startPos.ToVector3()).normalized;
|
||||
IntVec3 endPos = startPos + new IntVec3(
|
||||
(int)(direction.x * Props.flyOverDistance),
|
||||
0,
|
||||
(int)(direction.z * Props.flyOverDistance));
|
||||
|
||||
Log.Message($"Fixed distance position: {endPos} (from {startPos}, distance: {Props.flyOverDistance})");
|
||||
return endPos;
|
||||
}
|
||||
|
||||
private Vector3 GetDirectionFromCasterToTarget(LocalTargetInfo target)
|
||||
{
|
||||
Vector3 direction = (target.Cell.ToVector3() - parent.pawn.Position.ToVector3()).normalized;
|
||||
|
||||
if (direction == Vector3.zero)
|
||||
{
|
||||
direction = new Vector3(Rand.Range(-1f, 1f), 0, Rand.Range(-1f, 1f)).normalized;
|
||||
Log.Message($"Using random direction: {direction}");
|
||||
}
|
||||
|
||||
return direction;
|
||||
}
|
||||
|
||||
private void CreateStandardFlyOver(IntVec3 startPos, IntVec3 endPos)
|
||||
{
|
||||
ThingDef flyOverDef = Props.flyOverDef ?? DefDatabase<ThingDef>.GetNamedSilentFail("ARA_HiveShip");
|
||||
if (flyOverDef == null)
|
||||
{
|
||||
Log.Warning("No fly over def specified for standard fly over");
|
||||
return;
|
||||
}
|
||||
|
||||
FlyOver flyOver = FlyOver.MakeFlyOver(
|
||||
flyOverDef,
|
||||
startPos,
|
||||
endPos,
|
||||
parent.pawn.Map,
|
||||
Props.flightSpeed,
|
||||
Props.altitude
|
||||
);
|
||||
|
||||
flyOver.spawnContentsOnImpact = Props.dropContentsOnImpact;
|
||||
flyOver.playFlyOverSound = Props.playFlyOverSound;
|
||||
|
||||
if (Props.customSound != null)
|
||||
{
|
||||
// 自定义音效逻辑
|
||||
}
|
||||
|
||||
Log.Message($"Standard FlyOver created: {flyOver} from {startPos} to {endPos}");
|
||||
}
|
||||
|
||||
// 更新技能提示信息
|
||||
public override string ExtraLabelMouseAttachment(LocalTargetInfo target)
|
||||
{
|
||||
if (Props.enableGroundStrafing)
|
||||
{
|
||||
return $"扫射区域: {Props.strafeWidth * 2 + 1}格宽度";
|
||||
}
|
||||
else if (Props.enableSectorSurveillance)
|
||||
{
|
||||
return $"扇形监视: 约{Props.strafeWidth * 2 + 1}格宽度\n(具体参数在飞行物定义中)";
|
||||
}
|
||||
|
||||
return base.ExtraLabelMouseAttachment(target);
|
||||
}
|
||||
|
||||
public override bool Valid(LocalTargetInfo target, bool throwMessages = false)
|
||||
{
|
||||
return base.Valid(target, throwMessages) &&
|
||||
parent.pawn != null &&
|
||||
parent.pawn.Map != null &&
|
||||
target.Cell.IsValid &&
|
||||
target.Cell.InBounds(parent.pawn.Map);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
using UnityEngine;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_AbilitySpawnFlyOver : CompProperties_AbilityEffect
|
||||
{
|
||||
public ThingDef flyOverDef; // 飞越物体的 ThingDef
|
||||
public FlyOverType flyOverType = FlyOverType.Standard; // 飞越类型
|
||||
public ApproachType approachType = ApproachType.Standard; // 进场类型
|
||||
public float flightSpeed = 1f; // 飞行速度
|
||||
public float altitude = 15f; // 飞行高度
|
||||
public bool spawnContents = false; // 是否生成内容物
|
||||
public List<ThingDefCount> contents; // 内容物列表
|
||||
public bool dropContentsOnImpact = true; // 是否在终点投放内容物
|
||||
public SoundDef customSound; // 自定义音效
|
||||
public bool playFlyOverSound = true; // 是否播放飞越音效
|
||||
|
||||
// 起始位置选项(当approachType为Standard时使用)
|
||||
public StartPosition startPosition = StartPosition.Caster;
|
||||
public IntVec3 customStartOffset = IntVec3.Zero;
|
||||
|
||||
// 终点位置选项(当approachType为Standard时使用)
|
||||
public EndPosition endPosition = EndPosition.TargetCell;
|
||||
public IntVec3 customEndOffset = IntVec3.Zero;
|
||||
public int flyOverDistance = 30; // 飞越距离(当终点为自定义时)
|
||||
|
||||
// 地面扫射配置
|
||||
public bool enableGroundStrafing = false; // 是否启用地面扫射
|
||||
public int strafeWidth = 3; // 扫射宽度(用于预览)
|
||||
public int strafeLength = 15; // 扫射长度
|
||||
public float strafeFireChance = 0.7f; // 扫射发射概率
|
||||
public int minStrafeProjectiles = -1; // 新增:最小射弹数
|
||||
public int maxStrafeProjectiles = -1; // 新增:最大射弹数
|
||||
public ThingDef strafeProjectile; // 抛射体定义
|
||||
|
||||
// 地面扫射可视化
|
||||
public bool showStrafePreview = true; // 是否显示扫射预览
|
||||
public Color strafePreviewColor = new Color(1f, 0.3f, 0.3f, 0.3f);
|
||||
|
||||
// 扇形监视配置 - 只传递信号,不传递具体参数
|
||||
public bool enableSectorSurveillance = false; // 是否启用扇形区域监视
|
||||
|
||||
// 扇形监视可视化 - 使用strafeWidth来近似预览区域宽度
|
||||
public bool showSectorPreview = true; // 是否显示扇形预览
|
||||
public Color sectorPreviewColor = new Color(0.3f, 0.7f, 1f, 0.3f);
|
||||
|
||||
public CompProperties_AbilitySpawnFlyOver()
|
||||
{
|
||||
this.compClass = typeof(CompAbilityEffect_SpawnFlyOver);
|
||||
}
|
||||
}
|
||||
|
||||
// 飞越类型枚举
|
||||
public enum FlyOverType
|
||||
{
|
||||
Standard, // 标准飞越
|
||||
HighAltitude, // 高空飞越
|
||||
CargoDrop, // 货运飞越
|
||||
BombingRun, // 轰炸飞越
|
||||
Reconnaissance, // 侦察飞越
|
||||
GroundStrafing, // 地面扫射
|
||||
SectorSurveillance // 扇形区域监视
|
||||
}
|
||||
|
||||
// 进场类型枚举
|
||||
public enum ApproachType
|
||||
{
|
||||
Standard, // 标准进场(使用原有的位置计算)
|
||||
Perpendicular // 垂直线进场(垂直于施法者-目标连线)
|
||||
}
|
||||
|
||||
// 起始位置枚举
|
||||
public enum StartPosition
|
||||
{
|
||||
Caster, // 施法者位置
|
||||
MapEdge, // 地图边缘
|
||||
CustomOffset, // 自定义偏移
|
||||
RandomMapEdge // 随机地图边缘
|
||||
}
|
||||
|
||||
// 终点位置枚举
|
||||
public enum EndPosition
|
||||
{
|
||||
TargetCell, // 目标单元格
|
||||
OppositeMapEdge, // 对面地图边缘
|
||||
CustomOffset, // 自定义偏移
|
||||
FixedDistance, // 固定距离
|
||||
RandomMapEdge
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.MoharHediffs
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class HediffCompProperties_Spawner : HediffCompProperties
|
||||
{
|
||||
@@ -5,7 +5,7 @@ using RimWorld;
|
||||
using RimWorld.Planet;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.MoharHediffs
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class HediffComp_Spawner : HediffComp
|
||||
{
|
||||
@@ -4,7 +4,7 @@ using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.MoharHediffs
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public static class Tools
|
||||
{
|
||||
@@ -1,128 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class SpawnerProduct
|
||||
{
|
||||
public ThingDef thingDef;
|
||||
public int count = 1;
|
||||
}
|
||||
|
||||
// --- Properties Class ---
|
||||
public class CompProperties_MultiFuelSpawner : CompProperties
|
||||
{
|
||||
public List<SpawnerProduct> products;
|
||||
public IntRange spawnIntervalRange = new IntRange(100, 100);
|
||||
public bool spawnForbidden;
|
||||
public bool inheritFaction;
|
||||
public bool showMessageIfOwned;
|
||||
|
||||
public CompProperties_MultiFuelSpawner()
|
||||
{
|
||||
compClass = typeof(CompMultiFuelSpawner);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Component Class ---
|
||||
public class CompMultiFuelSpawner : ThingComp
|
||||
{
|
||||
private int ticksUntilSpawn;
|
||||
private List<CompRefuelableWithKey> fuelComps;
|
||||
|
||||
public CompProperties_MultiFuelSpawner Props => (CompProperties_MultiFuelSpawner)props;
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
base.PostSpawnSetup(respawningAfterLoad);
|
||||
if (!respawningAfterLoad)
|
||||
{
|
||||
ResetCountdown();
|
||||
}
|
||||
fuelComps = parent.GetComps<CompRefuelableWithKey>().ToList();
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref ticksUntilSpawn, "ticksUntilSpawn", 0);
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
if (fuelComps.NullOrEmpty()) return;
|
||||
|
||||
bool allFuelsOk = fuelComps.All(c => c.HasFuel);
|
||||
|
||||
if (allFuelsOk && (parent.GetComp<CompPowerTrader>()?.PowerOn ?? true))
|
||||
{
|
||||
ticksUntilSpawn--;
|
||||
if (ticksUntilSpawn <= 0)
|
||||
{
|
||||
foreach (var comp in fuelComps)
|
||||
{
|
||||
comp.Notify_UsedThisTick();
|
||||
}
|
||||
|
||||
TryDoSpawn();
|
||||
ResetCountdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TryDoSpawn()
|
||||
{
|
||||
if (Props.products.NullOrEmpty()) return;
|
||||
|
||||
foreach (var product in Props.products)
|
||||
{
|
||||
Thing thing = ThingMaker.MakeThing(product.thingDef);
|
||||
thing.stackCount = product.count;
|
||||
|
||||
if (Props.inheritFaction && thing.Faction != parent.Faction)
|
||||
{
|
||||
thing.SetFaction(parent.Faction);
|
||||
}
|
||||
|
||||
if (GenPlace.TryPlaceThing(thing, parent.Position, parent.Map, ThingPlaceMode.Near, out Thing resultingThing))
|
||||
{
|
||||
if (Props.spawnForbidden)
|
||||
{
|
||||
resultingThing.SetForbidden(true);
|
||||
}
|
||||
|
||||
if (Props.showMessageIfOwned && parent.Faction == Faction.OfPlayer)
|
||||
{
|
||||
Messages.Message("MessageCompSpawnerSpawnedItem".Translate(resultingThing.LabelCap), resultingThing, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetCountdown()
|
||||
{
|
||||
ticksUntilSpawn = Props.spawnIntervalRange.RandomInRange;
|
||||
}
|
||||
|
||||
public override string CompInspectStringExtra()
|
||||
{
|
||||
string text = base.CompInspectStringExtra();
|
||||
|
||||
if (fuelComps.All(c => c.HasFuel))
|
||||
{
|
||||
if (!text.NullOrEmpty())
|
||||
{
|
||||
text += "\n";
|
||||
}
|
||||
string productsStr = Props.products.Select(p => (string)p.thingDef.LabelCap).ToCommaList();
|
||||
text += "NextSpawnedItemIn".Translate(productsStr) + ": " + ticksUntilSpawn.ToStringTicksToPeriod();
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// --- Properties Class ---
|
||||
public class CompProperties_RefuelableSpawner : CompProperties_Refuelable
|
||||
{
|
||||
public List<SpawnerProduct> products;
|
||||
public IntRange spawnIntervalRange = new IntRange(100, 100);
|
||||
public bool spawnForbidden;
|
||||
public bool inheritFaction;
|
||||
public bool showMessageIfOwned;
|
||||
|
||||
public CompProperties_RefuelableSpawner()
|
||||
{
|
||||
compClass = typeof(CompRefuelableSpawner);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Component Class ---
|
||||
public class CompRefuelableSpawner : CompRefuelable
|
||||
{
|
||||
private int ticksUntilSpawn;
|
||||
|
||||
public new CompProperties_RefuelableSpawner Props => (CompProperties_RefuelableSpawner)props;
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref ticksUntilSpawn, "ticksUntilSpawn", 0);
|
||||
}
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
base.PostSpawnSetup(respawningAfterLoad);
|
||||
if (!respawningAfterLoad)
|
||||
{
|
||||
ResetCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
if (HasFuel && (parent.GetComp<CompPowerTrader>()?.PowerOn ?? true))
|
||||
{
|
||||
ticksUntilSpawn--;
|
||||
if (ticksUntilSpawn <= 0)
|
||||
{
|
||||
TryDoSpawn();
|
||||
ResetCountdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TryDoSpawn()
|
||||
{
|
||||
if (Props.products.NullOrEmpty()) return;
|
||||
|
||||
foreach (var product in Props.products)
|
||||
{
|
||||
Thing thing = ThingMaker.MakeThing(product.thingDef);
|
||||
thing.stackCount = product.count;
|
||||
|
||||
if (Props.inheritFaction && thing.Faction != parent.Faction)
|
||||
{
|
||||
thing.SetFaction(parent.Faction);
|
||||
}
|
||||
|
||||
if (GenPlace.TryPlaceThing(thing, parent.Position, parent.Map, ThingPlaceMode.Near, out Thing resultingThing))
|
||||
{
|
||||
if (Props.spawnForbidden)
|
||||
{
|
||||
resultingThing.SetForbidden(true);
|
||||
}
|
||||
|
||||
if (Props.showMessageIfOwned && parent.Faction == Faction.OfPlayer)
|
||||
{
|
||||
Messages.Message("MessageCompSpawnerSpawnedItem".Translate(resultingThing.LabelCap), resultingThing, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetCountdown()
|
||||
{
|
||||
ticksUntilSpawn = Props.spawnIntervalRange.RandomInRange;
|
||||
}
|
||||
|
||||
public override string CompInspectStringExtra()
|
||||
{
|
||||
string text = base.CompInspectStringExtra();
|
||||
|
||||
if (HasFuel)
|
||||
{
|
||||
if (!text.NullOrEmpty())
|
||||
{
|
||||
text += "\n";
|
||||
}
|
||||
string productsStr = Props.products.Select(p => (string)p.thingDef.LabelCap).ToCommaList();
|
||||
text += "NextSpawnedItemIn".Translate(productsStr) + ": " + ticksUntilSpawn.ToStringTicksToPeriod();
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
foreach (var g in base.CompGetGizmosExtra())
|
||||
{
|
||||
yield return g;
|
||||
}
|
||||
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "DEBUG: Spawn items",
|
||||
action = delegate
|
||||
{
|
||||
TryDoSpawn();
|
||||
ResetCountdown();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SpawnerProduct
|
||||
{
|
||||
public ThingDef thingDef;
|
||||
public int count = 1;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// 1. New Properties class that adds the save key
|
||||
public class CompProperties_RefuelableWithKey : CompProperties_Refuelable
|
||||
{
|
||||
public string saveKeysPrefix;
|
||||
|
||||
public CompProperties_RefuelableWithKey()
|
||||
{
|
||||
compClass = typeof(CompRefuelableWithKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. New Component class. It's empty for now.
|
||||
// Its purpose is to be a safe target for our Harmony patch.
|
||||
public class CompRefuelableWithKey : CompRefuelable
|
||||
{
|
||||
// We will override PostExposeData using a Harmony patch
|
||||
// to avoid re-implementing the entire class.
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// --- 1. The Properties Class ---
|
||||
public class CompProperties_Refuelable_WithKey : CompProperties
|
||||
{
|
||||
public float fuelConsumptionRate = 1f;
|
||||
public float fuelCapacity = 2f;
|
||||
public float initialFuelPercent;
|
||||
public float autoRefuelPercent = 0.3f;
|
||||
public ThingFilter fuelFilter;
|
||||
public bool consumeFuelOnlyWhenUsed = true;
|
||||
public bool showFuelGizmo = true;
|
||||
public bool targetFuelLevelConfigurable;
|
||||
public float initialConfigurableTargetFuelLevel = -1f;
|
||||
public string fuelLabel;
|
||||
public string outOfFuelMessage;
|
||||
public bool showAllowAutoRefuelToggle;
|
||||
public string saveKeysPrefix; // The only field we are adding
|
||||
|
||||
public CompProperties_Refuelable_WithKey()
|
||||
{
|
||||
compClass = typeof(CompRefuelable_WithKey);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. The Component Class (Full Re-implementation) ---
|
||||
public class CompRefuelable_WithKey : ThingComp
|
||||
{
|
||||
// Re-implemented fields from CompRefuelable
|
||||
private float fuel;
|
||||
private float configuredTargetFuelLevel = -1f;
|
||||
public bool allowAutoRefuel = true;
|
||||
private CompFlickable flickComp;
|
||||
|
||||
public new CompProperties_Refuelable_WithKey Props => (CompProperties_Refuelable_WithKey)props;
|
||||
|
||||
public float Fuel => fuel;
|
||||
public bool HasFuel => fuel > 0f;
|
||||
public bool IsFull => TargetFuelLevel - fuel < 1f;
|
||||
public float FuelPercentOfMax => fuel / Props.fuelCapacity;
|
||||
public float TargetFuelLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
if (configuredTargetFuelLevel >= 0f) return configuredTargetFuelLevel;
|
||||
if (Props.targetFuelLevelConfigurable) return Props.initialConfigurableTargetFuelLevel;
|
||||
return Props.fuelCapacity;
|
||||
}
|
||||
set => configuredTargetFuelLevel = Mathf.Clamp(value, 0f, Props.fuelCapacity);
|
||||
}
|
||||
public bool ShouldAutoRefuelNow => Fuel / TargetFuelLevel <= Props.autoRefuelPercent && !IsFull && TargetFuelLevel > 0f && (flickComp == null || flickComp.SwitchIsOn);
|
||||
|
||||
|
||||
public override void Initialize(CompProperties props)
|
||||
{
|
||||
base.Initialize(props);
|
||||
allowAutoRefuel = true; // Simplified from base
|
||||
fuel = Props.fuelCapacity * Props.initialFuelPercent;
|
||||
if(Props.initialConfigurableTargetFuelLevel > 0)
|
||||
{
|
||||
configuredTargetFuelLevel = Props.initialConfigurableTargetFuelLevel;
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
base.PostSpawnSetup(respawningAfterLoad);
|
||||
flickComp = parent.GetComp<CompFlickable>();
|
||||
}
|
||||
|
||||
// The ONLY method we actually change
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
string prefix = Props.saveKeysPrefix;
|
||||
if (prefix.NullOrEmpty())
|
||||
{
|
||||
Log.ErrorOnce($"CompRefuelable_WithKey on {parent.def.defName} has a null or empty saveKeysPrefix.", GetHashCode());
|
||||
// Fallback to default scribing to avoid data loss
|
||||
Scribe_Values.Look(ref fuel, "fuel", 0f);
|
||||
Scribe_Values.Look(ref configuredTargetFuelLevel, "configuredTargetFuelLevel", -1f);
|
||||
Scribe_Values.Look(ref allowAutoRefuel, "allowAutoRefuel", true);
|
||||
return;
|
||||
}
|
||||
Scribe_Values.Look(ref fuel, prefix + "_fuel", 0f);
|
||||
Scribe_Values.Look(ref configuredTargetFuelLevel, prefix + "_configuredTargetFuelLevel", -1f);
|
||||
Scribe_Values.Look(ref allowAutoRefuel, prefix + "_allowAutoRefuel", true);
|
||||
}
|
||||
|
||||
public void ConsumeFuel(float amount)
|
||||
{
|
||||
if (fuel <= 0f) return;
|
||||
fuel -= amount;
|
||||
if (fuel <= 0f)
|
||||
{
|
||||
fuel = 0f;
|
||||
parent.BroadcastCompSignal("RanOutOfFuel");
|
||||
}
|
||||
}
|
||||
|
||||
public void Refuel(float amount)
|
||||
{
|
||||
fuel += amount;
|
||||
if (fuel > Props.fuelCapacity)
|
||||
{
|
||||
fuel = Props.fuelCapacity;
|
||||
}
|
||||
parent.BroadcastCompSignal("Refueled");
|
||||
}
|
||||
|
||||
public void Notify_UsedThisTick()
|
||||
{
|
||||
if (Props.consumeFuelOnlyWhenUsed)
|
||||
{
|
||||
ConsumeFuel(Props.fuelConsumptionRate / 60000f);
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
if (!Props.showFuelGizmo || parent.Faction != Faction.OfPlayer) yield break;
|
||||
|
||||
// Simplified Gizmo Status (can be replaced with copied Gizmo_RefuelableFuelStatus later)
|
||||
yield return new Gizmo_FuelStatus_Spawner(new FuelSystem(this)); // Using a dummy adapter
|
||||
|
||||
// Copied Set Target Level Command
|
||||
if (Props.targetFuelLevelConfigurable)
|
||||
{
|
||||
var command = new Command_Action
|
||||
{
|
||||
defaultLabel = "CommandSetTargetFuelLevel".Translate(),
|
||||
defaultDesc = "CommandSetTargetFuelLevelDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/SetTargetFuelLevel"),
|
||||
action = delegate
|
||||
{
|
||||
Dialog_Slider dialog = new Dialog_Slider(
|
||||
"SetTargetFuelLevel".Translate(), 0, (int)Props.fuelCapacity,
|
||||
(val) => TargetFuelLevel = val, (int)TargetFuelLevel);
|
||||
Find.WindowStack.Add(dialog);
|
||||
}
|
||||
};
|
||||
yield return command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy adapter to make the new Gizmo work temporarily
|
||||
public class FuelSystem
|
||||
{
|
||||
public CompRefuelable_WithKey comp;
|
||||
public FuelSystem(CompRefuelable_WithKey comp) { this.comp = comp; }
|
||||
public float Fuel => comp.Fuel;
|
||||
public float FuelPercent => comp.FuelPercentOfMax;
|
||||
public CompProperties_Refuelable props => comp.Props;
|
||||
public float TargetFuelLevel { get => comp.TargetFuelLevel; set => comp.TargetFuelLevel = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// We patch the base class method
|
||||
[HarmonyPatch(typeof(CompRefuelable), "PostExposeData")]
|
||||
public static class Patch_CompRefuelableWithKey_PostExposeData
|
||||
{
|
||||
public static bool Prefix(CompRefuelable __instance)
|
||||
{
|
||||
// But we only act if the instance is our custom subclass
|
||||
if (!(__instance is CompRefuelableWithKey refuelableWithKey))
|
||||
{
|
||||
// If it's not our class, run the original method
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the private fields from the base CompRefuelable class using reflection
|
||||
FieldInfo fuelField = AccessTools.Field(typeof(CompRefuelable), "fuel");
|
||||
FieldInfo configuredTargetFuelLevelField = AccessTools.Field(typeof(CompRefuelable), "configuredTargetFuelLevel");
|
||||
FieldInfo allowAutoRefuelField = AccessTools.Field(typeof(CompRefuelable), "allowAutoRefuel");
|
||||
|
||||
// Get the props from our custom component
|
||||
var props = (CompProperties_RefuelableWithKey)refuelableWithKey.Props;
|
||||
string prefix = props.saveKeysPrefix;
|
||||
|
||||
if (prefix.NullOrEmpty())
|
||||
{
|
||||
Log.ErrorOnce($"CompRefuelableWithKey on {refuelableWithKey.parent.def.defName} has a null or empty saveKeysPrefix. Defaulting to standard save.", refuelableWithKey.GetHashCode());
|
||||
// If no prefix, let the original method run
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get current values from the instance
|
||||
float fuel = (float)fuelField.GetValue(refuelableWithKey);
|
||||
float configuredTargetFuelLevel = (float)configuredTargetFuelLevelField.GetValue(refuelableWithKey);
|
||||
bool allowAutoRefuel = (bool)allowAutoRefuelField.GetValue(refuelableWithKey);
|
||||
|
||||
// Scribe the values with our prefix
|
||||
Scribe_Values.Look(ref fuel, prefix + "_fuel", 0f);
|
||||
Scribe_Values.Look(ref configuredTargetFuelLevel, prefix + "_configuredTargetFuelLevel", -1f);
|
||||
Scribe_Values.Look(ref allowAutoRefuel, prefix + "_allowAutoRefuel", true);
|
||||
|
||||
// Set the new values back to the instance
|
||||
if (Scribe.mode == LoadSaveMode.LoadingVars)
|
||||
{
|
||||
fuelField.SetValue(refuelableWithKey, fuel);
|
||||
configuredTargetFuelLevelField.SetValue(refuelableWithKey, configuredTargetFuelLevel);
|
||||
allowAutoRefuelField.SetValue(refuelableWithKey, allowAutoRefuel);
|
||||
}
|
||||
|
||||
// Prevent the original PostExposeData from running
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user