整理scoure

This commit is contained in:
2025-10-31 09:57:45 +08:00
parent 9e6aa98830
commit 8fee1bcfba
103 changed files with 5547 additions and 916 deletions

Binary file not shown.

View File

@@ -317,7 +317,7 @@
</li>
</stages>
<comps>
<li Class="WulaFallenEmpire.MoharHediffs.HediffCompProperties_Spawner">
<li Class="WulaFallenEmpire.HediffCompProperties_Spawner">
<!--
==================================================
基础设置 (Basic Settings)

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

View File

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

View File

@@ -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();
// 不需要保存状态,因为建筑起飞后就销毁了
}
}
}

View File

@@ -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("===============================");
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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 // 正弦波(平滑波动)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
}

View File

@@ -1,7 +1,7 @@
using System;
using Verse;
namespace WulaFallenEmpire.MoharHediffs
namespace WulaFallenEmpire
{
public class HediffCompProperties_Spawner : HediffCompProperties
{

View File

@@ -5,7 +5,7 @@ using RimWorld;
using RimWorld.Planet;
using Verse;
namespace WulaFallenEmpire.MoharHediffs
namespace WulaFallenEmpire
{
public class HediffComp_Spawner : HediffComp
{

View File

@@ -4,7 +4,7 @@ using RimWorld;
using UnityEngine;
using Verse;
namespace WulaFallenEmpire.MoharHediffs
namespace WulaFallenEmpire
{
public static class Tools
{

View File

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

View File

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

View File

@@ -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.
}
}

View File

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

View File

@@ -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