1
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.AI.Group;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompAutoMechCarrier : CompMechCarrier
|
||||
{
|
||||
#region Reflected Fields
|
||||
private static FieldInfo spawnedPawnsField;
|
||||
private static FieldInfo cooldownTicksRemainingField;
|
||||
private static FieldInfo innerContainerField;
|
||||
|
||||
private List<Pawn> SpawnedPawns
|
||||
{
|
||||
get
|
||||
{
|
||||
if (spawnedPawnsField == null)
|
||||
spawnedPawnsField = typeof(CompMechCarrier).GetField("spawnedPawns", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
return (List<Pawn>)spawnedPawnsField.GetValue(this);
|
||||
}
|
||||
}
|
||||
|
||||
private int CooldownTicksRemaining
|
||||
{
|
||||
get
|
||||
{
|
||||
if (cooldownTicksRemainingField == null)
|
||||
cooldownTicksRemainingField = typeof(CompMechCarrier).GetField("cooldownTicksRemaining", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
return (int)cooldownTicksRemainingField.GetValue(this);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (cooldownTicksRemainingField == null)
|
||||
cooldownTicksRemainingField = typeof(CompMechCarrier).GetField("cooldownTicksRemaining", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
cooldownTicksRemainingField.SetValue(this, value);
|
||||
}
|
||||
}
|
||||
|
||||
private ThingOwner InnerContainer
|
||||
{
|
||||
get
|
||||
{
|
||||
if (innerContainerField == null)
|
||||
innerContainerField = typeof(CompMechCarrier).GetField("innerContainer", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
return (ThingOwner)innerContainerField.GetValue(this);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public CompProperties_AutoMechCarrier AutoProps => (CompProperties_AutoMechCarrier)props;
|
||||
|
||||
private int TotalPawnCapacity => AutoProps.productionQueue.Sum(e => e.count);
|
||||
|
||||
private int LiveSpawnedPawnsCount(PawnKindDef kind)
|
||||
{
|
||||
SpawnedPawns.RemoveAll(p => p == null || p.Destroyed);
|
||||
return SpawnedPawns.Count(p => p.kindDef == kind);
|
||||
}
|
||||
|
||||
private AcceptanceReport CanSpawnNow(PawnKindDef kind)
|
||||
{
|
||||
if (parent is Pawn pawn && (pawn.IsSelfShutdown() || !pawn.Awake() || pawn.Downed || pawn.Dead || !pawn.Spawned))
|
||||
return false;
|
||||
if (CooldownTicksRemaining > 0)
|
||||
return "CooldownTime".Translate() + " " + CooldownTicksRemaining.ToStringSecondsFromTicks();
|
||||
|
||||
PawnProductionEntry entry = AutoProps.productionQueue.First(e => e.pawnKind == kind);
|
||||
int cost = entry.cost ?? Props.costPerPawn;
|
||||
|
||||
if (!AutoProps.freeProduction && InnerContainer.TotalStackCountOfDef(Props.fixedIngredient) < cost)
|
||||
return "MechCarrierNotEnoughResources".Translate();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TrySpawnPawn(PawnKindDef kind)
|
||||
{
|
||||
PawnGenerationRequest request = new PawnGenerationRequest(kind, parent.Faction, PawnGenerationContext.NonPlayer, -1, forceGenerateNewPawn: true);
|
||||
Pawn pawn = PawnGenerator.GeneratePawn(request);
|
||||
GenSpawn.Spawn(pawn, parent.Position, parent.Map);
|
||||
SpawnedPawns.Add(pawn);
|
||||
|
||||
if (parent is Pawn p && p.GetLord() != null)
|
||||
p.GetLord().AddPawn(pawn);
|
||||
|
||||
if (!AutoProps.freeProduction)
|
||||
{
|
||||
PawnProductionEntry entry = AutoProps.productionQueue.First(e => e.pawnKind == kind);
|
||||
int costLeft = entry.cost ?? Props.costPerPawn;
|
||||
|
||||
List<Thing> things = new List<Thing>(InnerContainer);
|
||||
for (int j = 0; j < things.Count; j++)
|
||||
{
|
||||
Thing thing = InnerContainer.Take(things[j], Mathf.Min(things[j].stackCount, costLeft));
|
||||
costLeft -= thing.stackCount;
|
||||
thing.Destroy();
|
||||
if (costLeft <= 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
PawnProductionEntry spawnEntry = AutoProps.productionQueue.First(e => e.pawnKind == kind);
|
||||
CooldownTicksRemaining = spawnEntry.cooldownTicks ?? Props.cooldownTicks;
|
||||
|
||||
if (Props.spawnedMechEffecter != null)
|
||||
EffecterTrigger(Props.spawnedMechEffecter, Props.attachSpawnedMechEffecter, pawn);
|
||||
if (Props.spawnEffecter != null)
|
||||
EffecterTrigger(Props.spawnEffecter, Props.attachSpawnedEffecter, parent);
|
||||
}
|
||||
|
||||
private void EffecterTrigger(EffecterDef effecterDef, bool attach, Thing target)
|
||||
{
|
||||
Effecter effecter = new Effecter(effecterDef);
|
||||
effecter.Trigger(attach ? ((TargetInfo)target) : new TargetInfo(target.Position, target.Map), TargetInfo.Invalid);
|
||||
effecter.Cleanup();
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
if (parent.IsHashIntervalTick(60)) // 每秒检查一次
|
||||
{
|
||||
// 检查是否有抑制生产的Hediff
|
||||
if (AutoProps.disableHediff != null && (parent as Pawn)?.health.hediffSet.HasHediff(AutoProps.disableHediff) == true)
|
||||
{
|
||||
return; // 有Hediff,停止生产
|
||||
}
|
||||
|
||||
// 1. 先检查是否满员
|
||||
bool isFull = true;
|
||||
foreach (var entry in AutoProps.productionQueue)
|
||||
{
|
||||
if (LiveSpawnedPawnsCount(entry.pawnKind) < entry.count)
|
||||
{
|
||||
isFull = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFull)
|
||||
{
|
||||
return; // 如果已满员,则不进行任何操作,包括冷却计时
|
||||
}
|
||||
|
||||
// 2. 如果未满员,才检查冷却时间
|
||||
if (CooldownTicksRemaining > 0) return;
|
||||
|
||||
// 3. 寻找空位并生产
|
||||
foreach (var entry in AutoProps.productionQueue)
|
||||
{
|
||||
if (LiveSpawnedPawnsCount(entry.pawnKind) < entry.count)
|
||||
{
|
||||
if (CanSpawnNow(entry.pawnKind).Accepted)
|
||||
{
|
||||
TrySpawnPawn(entry.pawnKind);
|
||||
break; // 每次只生产一个
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
// 移除所有Gizmo逻辑
|
||||
return Enumerable.Empty<Gizmo>();
|
||||
}
|
||||
|
||||
public override string CompInspectStringExtra()
|
||||
{
|
||||
SpawnedPawns.RemoveAll(p => p == null || p.Destroyed);
|
||||
string text = "Pawns: " + SpawnedPawns.Count + " / " + TotalPawnCapacity;
|
||||
|
||||
foreach (var entry in AutoProps.productionQueue)
|
||||
{
|
||||
text += $"\n- {entry.pawnKind.LabelCap}: {LiveSpawnedPawnsCount(entry.pawnKind)} / {entry.count}";
|
||||
}
|
||||
|
||||
if (CooldownTicksRemaining > 0)
|
||||
{
|
||||
text += "\n" + "CooldownTime".Translate() + ": " + CooldownTicksRemaining.ToStringSecondsFromTicks();
|
||||
}
|
||||
|
||||
if (!AutoProps.freeProduction)
|
||||
{
|
||||
text += "\n" + base.CompInspectStringExtra();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_AutoMechCarrier : CompProperties_MechCarrier
|
||||
{
|
||||
// XML中定义,生产是否消耗资源
|
||||
public bool freeProduction = false;
|
||||
|
||||
// 如果单位拥有这个Hediff,则停止生产
|
||||
public HediffDef disableHediff;
|
||||
|
||||
// 定义生产队列
|
||||
public List<PawnProductionEntry> productionQueue = new List<PawnProductionEntry>();
|
||||
|
||||
public CompProperties_AutoMechCarrier()
|
||||
{
|
||||
// 确保这个属性类指向我们新的功能实现类
|
||||
compClass = typeof(CompAutoMechCarrier);
|
||||
}
|
||||
|
||||
public override IEnumerable<string> ConfigErrors(ThingDef parentDef)
|
||||
{
|
||||
foreach (string error in base.ConfigErrors(parentDef))
|
||||
{
|
||||
yield return error;
|
||||
}
|
||||
|
||||
if (productionQueue.NullOrEmpty())
|
||||
{
|
||||
yield return "CompProperties_AutoMechCarrier must have at least one entry in productionQueue.";
|
||||
}
|
||||
}
|
||||
|
||||
public override void ResolveReferences(ThingDef parentDef)
|
||||
{
|
||||
base.ResolveReferences(parentDef);
|
||||
// Prevent division by zero if costPerPawn is not set, which the base game AI might try to access.
|
||||
if (costPerPawn <= 0)
|
||||
{
|
||||
costPerPawn = 1;
|
||||
}
|
||||
|
||||
// 如果spawnPawnKind为空(因为我们用了新的队列系统),
|
||||
// 就从队列里取第一个作为“假”值,以防止基类方法在生成Gizmo标签时出错。
|
||||
if (spawnPawnKind == null && !productionQueue.NullOrEmpty())
|
||||
{
|
||||
spawnPawnKind = productionQueue[0].pawnKind;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// A data class to hold information about a pawn to be produced in a queue.
|
||||
/// Used in XML definitions.
|
||||
/// </summary>
|
||||
public class PawnProductionEntry
|
||||
{
|
||||
// The PawnKindDef of the unit to spawn.
|
||||
public PawnKindDef pawnKind;
|
||||
|
||||
// The maximum number of this kind of unit to maintain.
|
||||
public int count = 1;
|
||||
|
||||
// Optional: specific cooldown for this entry. If not set, the parent comp's cooldown is used.
|
||||
public int? cooldownTicks;
|
||||
|
||||
// Optional: specific cost for this entry. If not set, the parent comp's costPerPawn is used.
|
||||
public int? cost;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// 自定义条件节点:检查是否处于自主工作模式
|
||||
public class ThinkNode_ConditionalAutonomousMech : ThinkNode_Conditional
|
||||
{
|
||||
protected override bool Satisfied(Pawn pawn)
|
||||
{
|
||||
if (pawn == null || pawn.Dead || pawn.Downed)
|
||||
return false;
|
||||
|
||||
// 检查是否被征召
|
||||
if (pawn.Drafted)
|
||||
return false;
|
||||
|
||||
// 检查是否有机械师控制
|
||||
if (pawn.GetOverseer() != null)
|
||||
return false;
|
||||
|
||||
// 检查是否有自主能力
|
||||
var comp = pawn.GetComp<CompAutonomousMech>();
|
||||
if (comp == null || !comp.CanWorkAutonomously)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class CompProperties_AutonomousMech : CompProperties
|
||||
{
|
||||
public bool enableAutonomousDrafting = true;
|
||||
public bool enableAutonomousWork = true;
|
||||
public bool requirePowerForAutonomy = true;
|
||||
public bool suppressUncontrolledWarning = true;
|
||||
|
||||
// 保留能量管理设置供 ThinkNode 使用
|
||||
public float lowEnergyThreshold = 0.3f; // 低能量阈值
|
||||
public float criticalEnergyThreshold = 0.1f; // 临界能量阈值
|
||||
public float rechargeCompleteThreshold = 0.9f; // 充电完成阈值
|
||||
|
||||
public MechWorkModeDef initialWorkMode;
|
||||
|
||||
public CompProperties_AutonomousMech()
|
||||
{
|
||||
compClass = typeof(CompAutonomousMech);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompAutonomousMech : ThingComp
|
||||
{
|
||||
public CompProperties_AutonomousMech Props => (CompProperties_AutonomousMech)props;
|
||||
|
||||
public Pawn MechPawn => parent as Pawn;
|
||||
|
||||
private MechWorkModeDef currentWorkMode;
|
||||
|
||||
public bool CanBeAutonomous
|
||||
{
|
||||
get
|
||||
{
|
||||
if (MechPawn == null || MechPawn.Dead )
|
||||
return false;
|
||||
|
||||
if (!Props.enableAutonomousDrafting)
|
||||
return false;
|
||||
|
||||
if (MechPawn.GetOverseer() != null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanWorkAutonomously
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Props.enableAutonomousWork)
|
||||
return false;
|
||||
|
||||
if (!CanBeAutonomous)
|
||||
return false;
|
||||
|
||||
if (MechPawn.Drafted)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldSuppressUncontrolledWarning
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Props.suppressUncontrolledWarning)
|
||||
return false;
|
||||
|
||||
return CanBeAutonomous;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsInCombatMode
|
||||
{
|
||||
get
|
||||
{
|
||||
if (MechPawn == null || MechPawn.Dead || MechPawn.Downed)
|
||||
return false;
|
||||
// 被征召或处于自主战斗模式
|
||||
return MechPawn.Drafted || (CanFightAutonomously && MechPawn.mindState?.duty?.def == DutyDefOf.AssaultColony);
|
||||
}
|
||||
}
|
||||
|
||||
// 在 CompAutonomousMech 类中添加这个新属性
|
||||
public bool CanFightAutonomously
|
||||
{
|
||||
get
|
||||
{
|
||||
if (MechPawn == null || MechPawn.Dead || MechPawn.Downed)
|
||||
return false;
|
||||
|
||||
if (!Props.enableAutonomousDrafting)
|
||||
return false;
|
||||
|
||||
if (MechPawn.GetOverseer() != null)
|
||||
return false;
|
||||
|
||||
if (!MechPawn.drafter?.Drafted == true)
|
||||
return false;
|
||||
|
||||
if (Props.requirePowerForAutonomy)
|
||||
{
|
||||
if (GetEnergyLevel() < Props.criticalEnergyThreshold)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public MechWorkModeDef CurrentWorkMode => currentWorkMode;
|
||||
|
||||
// 新增:能量状态检查方法
|
||||
public float GetEnergyLevel()
|
||||
{
|
||||
var energyNeed = MechPawn.needs?.TryGetNeed<Need_MechEnergy>();
|
||||
return energyNeed?.CurLevelPercentage ?? 0f;
|
||||
}
|
||||
|
||||
public bool IsLowEnergy => GetEnergyLevel() < Props.lowEnergyThreshold;
|
||||
public bool IsCriticalEnergy => GetEnergyLevel() < Props.criticalEnergyThreshold;
|
||||
public bool IsFullyCharged => GetEnergyLevel() >= Props.rechargeCompleteThreshold;
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
base.PostSpawnSetup(respawningAfterLoad);
|
||||
|
||||
if (currentWorkMode == null)
|
||||
{
|
||||
currentWorkMode = Props.initialWorkMode ?? MechWorkModeDefOf.Work;
|
||||
}
|
||||
|
||||
// 确保使用独立战斗系统
|
||||
InitializeAutonomousCombat();
|
||||
}
|
||||
|
||||
private void InitializeAutonomousCombat()
|
||||
{
|
||||
// 确保有 draftController
|
||||
if (MechPawn.drafter == null)
|
||||
{
|
||||
MechPawn.drafter = new Pawn_DraftController(MechPawn);
|
||||
}
|
||||
|
||||
// 强制启用 FireAtWill
|
||||
if (MechPawn.drafter != null)
|
||||
{
|
||||
MechPawn.drafter.FireAtWill = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
// 每60 tick检查一次能量状态
|
||||
if (MechPawn != null && MechPawn.IsColonyMech && Find.TickManager.TicksGame % 60 == 0)
|
||||
{
|
||||
// 删除了自动切换模式的 CheckEnergyStatus 调用
|
||||
EnsureWorkSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// 删除了整个 CheckEnergyStatus 方法,因为充电逻辑在 ThinkNode 中处理
|
||||
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
if (MechPawn == null || !CanBeAutonomous)
|
||||
yield break;
|
||||
|
||||
// 工作模式切换按钮
|
||||
if (CanWorkAutonomously)
|
||||
{
|
||||
DroneGizmo droneGizmo = new DroneGizmo(this);
|
||||
if (droneGizmo != null)
|
||||
{
|
||||
yield return droneGizmo;
|
||||
}
|
||||
}
|
||||
// 更换武器按钮 - 确保不返回null
|
||||
Gizmo weaponSwitchGizmo = CreateWeaponSwitchGizmo();
|
||||
if (weaponSwitchGizmo != null)
|
||||
{
|
||||
yield return weaponSwitchGizmo;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建更换武器的Gizmo
|
||||
/// </summary>
|
||||
private Gizmo CreateWeaponSwitchGizmo()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查Pawn是否属于玩家派系
|
||||
if (MechPawn?.Faction != Faction.OfPlayer)
|
||||
{
|
||||
return null; // 非玩家派系时不显示
|
||||
}
|
||||
// 检查Pawn是否有效
|
||||
if (MechPawn == null || MechPawn.Dead || MechPawn.Destroyed)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// 检查equipment是否有效
|
||||
if (MechPawn.equipment == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Command_Action switchWeaponCommand = new Command_Action
|
||||
{
|
||||
defaultLabel = "WULA_SwitchWeapon".Translate(),
|
||||
defaultDesc = "WULA_SwitchWeapon_Desc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("Wula/UI/Abilities/WULA_WeaponSwitchAbility", false) ?? BaseContent.BadTex,
|
||||
action = SwitchWeapon,
|
||||
};
|
||||
// 确保Command不为null
|
||||
if (switchWeaponCommand == null)
|
||||
{
|
||||
WulaLog.Debug($"Failed to create weapon switch gizmo for {MechPawn?.LabelCap}");
|
||||
return null;
|
||||
}
|
||||
return switchWeaponCommand;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
WulaLog.Debug($"Error creating weapon switch gizmo: {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更换武器逻辑
|
||||
/// </summary>
|
||||
private void SwitchWeapon()
|
||||
{
|
||||
if (MechPawn == null || MechPawn.Destroyed || !MechPawn.Spawned)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 扔掉当前武器
|
||||
ThingWithComps currentWeapon = MechPawn.equipment?.Primary;
|
||||
if (currentWeapon != null)
|
||||
{
|
||||
// 将武器扔在地上
|
||||
MechPawn.equipment.TryDropEquipment(currentWeapon, out ThingWithComps droppedWeapon, MechPawn.Position, true);
|
||||
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug($"[CompAutonomousMech] {MechPawn.LabelCap} dropped weapon: {currentWeapon.LabelCap}");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从PawnKind允许的武器中生成新武器
|
||||
ThingDef newWeaponDef = GetRandomWeaponFromPawnKind();
|
||||
if (newWeaponDef != null)
|
||||
{
|
||||
// 生成新武器
|
||||
Thing newWeapon = ThingMaker.MakeThing(newWeaponDef);
|
||||
if (newWeapon is ThingWithComps newWeaponWithComps)
|
||||
{
|
||||
// 使用 AddEquipment 方法装备新武器
|
||||
MechPawn.equipment.AddEquipment(newWeaponWithComps);
|
||||
|
||||
Messages.Message("WULA_WeaponSwitched".Translate(MechPawn.LabelCap, newWeaponDef.LabelCap),
|
||||
MechPawn, MessageTypeDefOf.PositiveEvent);
|
||||
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug($"[CompAutonomousMech] {MechPawn.LabelCap} equipped new weapon: {newWeaponDef.LabelCap}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Messages.Message("WULA_NoWeaponAvailable".Translate(MechPawn.LabelCap),
|
||||
MechPawn, MessageTypeDefOf.NegativeEvent);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
WulaLog.Debug($"[CompAutonomousMech] Error switching weapon for {MechPawn?.LabelCap}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从PawnKind允许的武器中随机获取一个武器定义
|
||||
/// </summary>
|
||||
private ThingDef GetRandomWeaponFromPawnKind()
|
||||
{
|
||||
if (MechPawn.kindDef?.weaponTags == null || MechPawn.kindDef.weaponTags.Count == 0)
|
||||
return null;
|
||||
|
||||
// 收集所有匹配的武器
|
||||
List<ThingDef> availableWeapons = new List<ThingDef>();
|
||||
|
||||
foreach (string weaponTag in MechPawn.kindDef.weaponTags)
|
||||
{
|
||||
foreach (ThingDef thingDef in DefDatabase<ThingDef>.AllDefs)
|
||||
{
|
||||
if (thingDef.IsWeapon && thingDef.weaponTags != null && thingDef.weaponTags.Contains(weaponTag))
|
||||
{
|
||||
availableWeapons.Add(thingDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (availableWeapons.Count == 0)
|
||||
return null;
|
||||
|
||||
// 随机选择一个武器
|
||||
return availableWeapons.RandomElement();
|
||||
}
|
||||
|
||||
public void SetWorkMode(MechWorkModeDef mode)
|
||||
{
|
||||
currentWorkMode = mode;
|
||||
|
||||
// 清除当前工作,让机械族重新选择符合新模式的工作
|
||||
if (MechPawn.CurJob != null && MechPawn.CurJob.def != JobDefOf.Wait_Combat)
|
||||
{
|
||||
MechPawn.jobs.StopAll();
|
||||
}
|
||||
|
||||
Messages.Message("WULA_SwitchedToMode".Translate(MechPawn.LabelCap, mode.label),
|
||||
MechPawn, MessageTypeDefOf.NeutralEvent);
|
||||
}
|
||||
|
||||
private void EnsureWorkSettings()
|
||||
{
|
||||
if (MechPawn.workSettings == null)
|
||||
{
|
||||
MechPawn.workSettings = new Pawn_WorkSettings(MechPawn);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetAutonomousStatusString()
|
||||
{
|
||||
if (!CanBeAutonomous)
|
||||
return null;
|
||||
|
||||
string energyInfo = "WULA_EnergyInfoShort".Translate(GetEnergyLevel().ToStringPercent());
|
||||
|
||||
if (MechPawn.Drafted)
|
||||
return "WULA_Autonomous_Drafted".Translate() + energyInfo;
|
||||
else
|
||||
return "WULA_Autonomous_Mode".Translate(currentWorkMode?.label ?? "Unknown") + energyInfo;
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Defs.Look(ref currentWorkMode, "currentWorkMode");
|
||||
// 删除了 wasLowEnergy 的序列化
|
||||
}
|
||||
}
|
||||
}
|
||||
115
Source/WulaFallenEmpire/Pawn_Comps/AutonomousMech/DroneGizmo.cs
Normal file
115
Source/WulaFallenEmpire/Pawn_Comps/AutonomousMech/DroneGizmo.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
[StaticConstructorOnStartup]
|
||||
public class DroneGizmo : Gizmo
|
||||
{
|
||||
private CompAutonomousMech comp;
|
||||
private HashSet<CompAutonomousMech> groupedComps;
|
||||
|
||||
private static readonly Texture2D BarTex = SolidColorMaterials.NewSolidColorTexture(new Color(0.34f, 0.42f, 0.43f));
|
||||
private static readonly Texture2D BarHighlightTex = SolidColorMaterials.NewSolidColorTexture(new Color(0.43f, 0.54f, 0.55f));
|
||||
private static readonly Texture2D EmptyBarTex = SolidColorMaterials.NewSolidColorTexture(new Color(0.03f, 0.035f, 0.05f));
|
||||
// private static readonly Texture2D DragBarTex = SolidColorMaterials.NewSolidColorTexture(new Color(0.74f, 0.97f, 0.8f));
|
||||
|
||||
// private static bool draggingBar;
|
||||
|
||||
public DroneGizmo(CompAutonomousMech comp)
|
||||
{
|
||||
this.comp = comp;
|
||||
}
|
||||
|
||||
public override float GetWidth(float maxWidth)
|
||||
{
|
||||
return 160f;
|
||||
}
|
||||
|
||||
public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms)
|
||||
{
|
||||
Rect rect = new Rect(topLeft.x, topLeft.y, GetWidth(maxWidth), 75f);
|
||||
Rect rect2 = rect.ContractedBy(10f);
|
||||
Widgets.DrawWindowBackground(rect);
|
||||
|
||||
string text = "WULA_AutonomousMech".Translate();
|
||||
Rect rect3 = new Rect(rect2.x, rect2.y, rect2.width, Text.CalcHeight(text, rect2.width) + 8f);
|
||||
Text.Font = GameFont.Small;
|
||||
Widgets.Label(rect3, text);
|
||||
|
||||
Rect rect4 = new Rect(rect2.x, rect3.yMax, rect2.width, rect2.height - rect3.height);
|
||||
DraggableBarForGroup(rect4);
|
||||
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
string energyText = comp.GetEnergyLevel().ToStringPercent();
|
||||
Widgets.Label(rect4, energyText);
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
|
||||
TooltipHandler.TipRegion(rect4, () => "WULA_EnergyInfo".Translate(energyText), Gen.HashCombineInt(comp.GetHashCode(), 34242419));
|
||||
|
||||
// Work Mode Button
|
||||
Rect rect6 = new Rect(rect2.x + rect2.width - 24f, rect2.y, 24f, 24f);
|
||||
if (Widgets.ButtonImageFitted(rect6, comp.CurrentWorkMode?.uiIcon ?? BaseContent.BadTex))
|
||||
{
|
||||
Find.WindowStack.Add(new FloatMenu(GetWorkModeOptions(comp, groupedComps).ToList()));
|
||||
}
|
||||
TooltipHandler.TipRegion(rect6, "WULA_Switch_Mech_WorkMode".Translate());
|
||||
Widgets.DrawHighlightIfMouseover(rect6);
|
||||
|
||||
return new GizmoResult(GizmoState.Clear);
|
||||
}
|
||||
|
||||
private void DraggableBarForGroup(Rect rect)
|
||||
{
|
||||
// We are not actually dragging the energy level, but maybe a threshold?
|
||||
// For now, just display the energy level.
|
||||
// If we want to set recharge threshold, we need a property in CompAutonomousMech for that.
|
||||
// Assuming we want to visualize energy level:
|
||||
|
||||
Widgets.FillableBar(rect, comp.GetEnergyLevel(), BarTex, EmptyBarTex, false);
|
||||
}
|
||||
|
||||
public static IEnumerable<FloatMenuOption> GetWorkModeOptions(CompAutonomousMech comp, HashSet<CompAutonomousMech> groupedComps = null)
|
||||
{
|
||||
foreach (MechWorkModeDef mode in DefDatabase<MechWorkModeDef>.AllDefs.OrderBy(d => d.uiOrder))
|
||||
{
|
||||
yield return new FloatMenuOption(mode.LabelCap, delegate
|
||||
{
|
||||
comp.SetWorkMode(mode);
|
||||
if (groupedComps != null)
|
||||
{
|
||||
foreach (CompAutonomousMech groupedComp in groupedComps)
|
||||
{
|
||||
groupedComp.SetWorkMode(mode);
|
||||
}
|
||||
}
|
||||
}, mode.uiIcon, Color.white);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool GroupsWith(Gizmo other)
|
||||
{
|
||||
return other is DroneGizmo;
|
||||
}
|
||||
|
||||
public override void MergeWith(Gizmo other)
|
||||
{
|
||||
base.MergeWith(other);
|
||||
if (other is DroneGizmo droneGizmo)
|
||||
{
|
||||
if (groupedComps == null)
|
||||
{
|
||||
groupedComps = new HashSet<CompAutonomousMech>();
|
||||
}
|
||||
groupedComps.Add(droneGizmo.comp);
|
||||
if (droneGizmo.groupedComps != null)
|
||||
{
|
||||
groupedComps.AddRange(droneGizmo.groupedComps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class JobGiver_DroneSelfShutdown : ThinkNode_JobGiver
|
||||
{
|
||||
protected override Job TryGiveJob(Pawn pawn)
|
||||
{
|
||||
if (!RCellFinder.TryFindNearbyMechSelfShutdownSpot(pawn.Position, pawn, pawn.Map, out var result, allowForbidden: true))
|
||||
{
|
||||
result = pawn.Position;
|
||||
}
|
||||
Job job = JobMaker.MakeJob(JobDefOf.SelfShutdown, result);
|
||||
job.forceSleep = true;
|
||||
return job;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
[StaticConstructorOnStartup]
|
||||
public class PawnColumnWorker_DroneEnergy : PawnColumnWorker
|
||||
{
|
||||
private const int Width = 120;
|
||||
|
||||
private const int BarPadding = 4;
|
||||
|
||||
public static readonly Texture2D EnergyBarTex = SolidColorMaterials.NewSolidColorTexture(new Color32(252, byte.MaxValue, byte.MaxValue, 65));
|
||||
|
||||
public override void DoCell(Rect rect, Pawn pawn, PawnTable table)
|
||||
{
|
||||
CompAutonomousMech comp = pawn.TryGetComp<CompAutonomousMech>();
|
||||
if (comp == null || !comp.CanBeAutonomous)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Widgets.FillableBar(rect.ContractedBy(4f), comp.GetEnergyLevel(), EnergyBarTex, BaseContent.ClearTex, doBorder: false);
|
||||
Text.Font = GameFont.Small;
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Widgets.Label(rect, comp.GetEnergyLevel().ToStringPercent());
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
Text.Font = GameFont.Small;
|
||||
}
|
||||
|
||||
public override int GetMinWidth(PawnTable table)
|
||||
{
|
||||
return Mathf.Max(base.GetMinWidth(table), 120);
|
||||
}
|
||||
|
||||
public override int GetMaxWidth(PawnTable table)
|
||||
{
|
||||
return Mathf.Min(base.GetMaxWidth(table), GetMinWidth(table));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class PawnColumnWorker_DroneWorkMode : PawnColumnWorker_Icon
|
||||
{
|
||||
protected override int Padding => 0;
|
||||
|
||||
public override void DoCell(Rect rect, Pawn pawn, PawnTable table)
|
||||
{
|
||||
CompAutonomousMech comp = pawn.TryGetComp<CompAutonomousMech>();
|
||||
if (comp == null || !comp.CanBeAutonomous)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Widgets.ButtonInvisible(rect))
|
||||
{
|
||||
Find.WindowStack.Add(new FloatMenu(DroneGizmo.GetWorkModeOptions(comp).ToList()));
|
||||
}
|
||||
base.DoCell(rect, pawn, table);
|
||||
}
|
||||
|
||||
protected override Texture2D GetIconFor(Pawn pawn)
|
||||
{
|
||||
return pawn?.TryGetComp<CompAutonomousMech>()?.CurrentWorkMode?.uiIcon;
|
||||
}
|
||||
|
||||
protected override string GetIconTip(Pawn pawn)
|
||||
{
|
||||
string text = pawn.TryGetComp<CompAutonomousMech>()?.CurrentWorkMode?.description;
|
||||
if (!text.NullOrEmpty())
|
||||
{
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class ThinkNode_ConditionalAutonomousWorkMode : ThinkNode_Conditional
|
||||
{
|
||||
public MechWorkModeDef requiredMode;
|
||||
|
||||
protected override bool Satisfied(Pawn pawn)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (pawn == null || pawn.Dead || pawn.Downed)
|
||||
return false;
|
||||
|
||||
// 检查是否有自主机械组件
|
||||
var comp = pawn.GetComp<CompAutonomousMech>();
|
||||
if (comp == null)
|
||||
return false;
|
||||
|
||||
// 检查当前工作模式是否匹配要求
|
||||
if (comp.CurrentWorkMode != requiredMode)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
WulaLog.Debug($"[WULA] Exception in ThinkNode_ConditionalAutonomousWorkMode.Satisfied for pawn {pawn?.LabelShort}: {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class ThinkNode_ConditionalLowEnergy_Drone : ThinkNode_Conditional
|
||||
{
|
||||
protected override bool Satisfied(Pawn pawn)
|
||||
{
|
||||
CompAutonomousMech compDrone = pawn.TryGetComp<CompAutonomousMech>();
|
||||
if (compDrone != null && compDrone.IsLowEnergy)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// 检查机械族是否需要充电
|
||||
public class ThinkNode_ConditionalNeedRecharge : ThinkNode_Conditional
|
||||
{
|
||||
public float energyThreshold = 0.3f;
|
||||
|
||||
protected override bool Satisfied(Pawn pawn)
|
||||
{
|
||||
if (pawn == null || pawn.Dead || pawn.Downed)
|
||||
return false;
|
||||
|
||||
var energyNeed = pawn.needs?.TryGetNeed<Need_MechEnergy>();
|
||||
if (energyNeed == null)
|
||||
return false;
|
||||
|
||||
return energyNeed.CurLevelPercentage < energyThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查机械族是否紧急需要充电
|
||||
public class ThinkNode_ConditionalEmergencyRecharge : ThinkNode_Conditional
|
||||
{
|
||||
public float emergencyThreshold = 0.1f;
|
||||
|
||||
protected override bool Satisfied(Pawn pawn)
|
||||
{
|
||||
if (pawn == null || pawn.Dead || pawn.Downed)
|
||||
return false;
|
||||
|
||||
var energyNeed = pawn.needs?.TryGetNeed<Need_MechEnergy>();
|
||||
if (energyNeed == null)
|
||||
return false;
|
||||
|
||||
return energyNeed.CurLevelPercentage < emergencyThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查机械族是否充满电
|
||||
public class ThinkNode_ConditionalFullyCharged : ThinkNode_Conditional
|
||||
{
|
||||
public float fullChargeThreshold = 0.9f;
|
||||
|
||||
protected override bool Satisfied(Pawn pawn)
|
||||
{
|
||||
if (pawn == null || pawn.Dead || pawn.Downed)
|
||||
return false;
|
||||
|
||||
var energyNeed = pawn.needs?.TryGetNeed<Need_MechEnergy>();
|
||||
if (energyNeed == null)
|
||||
return false;
|
||||
|
||||
return energyNeed.CurLevelPercentage >= fullChargeThreshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class ThinkNode_ConditionalWorkMode_Drone : ThinkNode_Conditional
|
||||
{
|
||||
public MechWorkModeDef workMode;
|
||||
|
||||
public override ThinkNode DeepCopy(bool resolve = true)
|
||||
{
|
||||
ThinkNode_ConditionalWorkMode_Drone thinkNode_ConditionalWorkMode_Drone = (ThinkNode_ConditionalWorkMode_Drone)base.DeepCopy(resolve);
|
||||
thinkNode_ConditionalWorkMode_Drone.workMode = workMode;
|
||||
return thinkNode_ConditionalWorkMode_Drone;
|
||||
}
|
||||
|
||||
protected override bool Satisfied(Pawn pawn)
|
||||
{
|
||||
if (!pawn.RaceProps.IsMechanoid || pawn.Faction != Faction.OfPlayer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
CompAutonomousMech compDrone = pawn.TryGetComp<CompAutonomousMech>();
|
||||
if (compDrone == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return compDrone.CurrentWorkMode == workMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
using Verse.AI.Group;
|
||||
using Verse.Sound;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompFighterInvisible : ThingComp
|
||||
{
|
||||
public CompProperties_FighterInvisible Props => (CompProperties_FighterInvisible)props;
|
||||
[Unsaved(false)]
|
||||
private HediffComp_Invisibility invisibility;
|
||||
private int lastDetectedTick = -99999;
|
||||
private int lastRevealedTick = -99999;
|
||||
private Pawn Sightstealer => (Pawn)parent;
|
||||
|
||||
// 新增:记录最后一次检查敌人的时间
|
||||
private int lastEnemyCheckTick = -99999;
|
||||
|
||||
// 新增:记录最后一次发信的时间
|
||||
private int lastLetterTick = -99999;
|
||||
|
||||
public HediffDef GetTargetInvisibilityDef()
|
||||
{
|
||||
return Props.InvisibilityDef;
|
||||
}
|
||||
|
||||
// 添加一个属性来检查是否有效
|
||||
private bool IsValid => Sightstealer?.health?.hediffSet != null &&
|
||||
GetTargetInvisibilityDef() != null &&
|
||||
!Sightstealer.IsShambler &&
|
||||
Sightstealer.Spawned &&
|
||||
Sightstealer.Map != null;
|
||||
|
||||
private HediffComp_Invisibility Invisibility
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsValid) return null;
|
||||
return invisibility ?? (invisibility = Sightstealer.health.hediffSet
|
||||
.GetFirstHediffOfDef(GetTargetInvisibilityDef())
|
||||
?.TryGetComp<HediffComp_Invisibility>());
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref lastDetectedTick, "lastDetectedTick", 0);
|
||||
Scribe_Values.Look(ref lastRevealedTick, "lastRevealedTick", 0);
|
||||
Scribe_Values.Look(ref lastEnemyCheckTick, "lastEnemyCheckTick", 0);
|
||||
Scribe_Values.Look(ref lastLetterTick, "lastLetterTick", 0);
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
// 使用统一的有效性检查
|
||||
if (!IsValid || Invisibility == null) return;
|
||||
|
||||
// 检查敌人
|
||||
CheckForEnemiesInSight();
|
||||
|
||||
// 隐身恢复逻辑
|
||||
if (Sightstealer.IsHashIntervalTick(Props.CheckDetectedIntervalTicks))
|
||||
{
|
||||
if (Find.TickManager.TicksGame > lastDetectedTick + Props.stealthCooldownTicks)
|
||||
{
|
||||
Invisibility.BecomeInvisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Notify_UsedVerb(Pawn pawn, Verb verb)
|
||||
{
|
||||
base.Notify_UsedVerb(pawn, verb);
|
||||
|
||||
// 统一的 null 检查
|
||||
if (Invisibility == null) return;
|
||||
|
||||
Invisibility.BecomeVisible();
|
||||
lastDetectedTick = Find.TickManager.TicksGame;
|
||||
|
||||
// 触发显现事件
|
||||
TrySendLetter("attack");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查视线内是否有敌人
|
||||
/// </summary>
|
||||
private void CheckForEnemiesInSight()
|
||||
{
|
||||
// 检查频率:每30 tick检查一次(约0.5秒)
|
||||
if (!Sightstealer.IsHashIntervalTick(30) ||
|
||||
Find.TickManager.TicksGame <= lastEnemyCheckTick + 30)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lastEnemyCheckTick = Find.TickManager.TicksGame;
|
||||
|
||||
// 检查视线内是否有敌人
|
||||
bool enemyInSight = false;
|
||||
List<Pawn> enemiesInSight = new List<Pawn>();
|
||||
|
||||
// 获取地图上所有Pawn
|
||||
IReadOnlyList<Pawn> allPawns = Sightstealer.Map.mapPawns.AllPawnsSpawned;
|
||||
|
||||
foreach (Pawn otherPawn in allPawns)
|
||||
{
|
||||
// 跳过自身
|
||||
if (otherPawn == Sightstealer) continue;
|
||||
|
||||
// 跳过自律机械
|
||||
if (otherPawn.GetComp<CompAutonomousMech>() != null) continue;
|
||||
|
||||
// 跳过死亡的
|
||||
if (otherPawn.Dead) continue;
|
||||
|
||||
// 跳过倒地的(如果配置为忽略)
|
||||
if (Props.ignoreDownedEnemies && otherPawn.Downed) continue;
|
||||
|
||||
// 跳过睡着的(如果配置为忽略)
|
||||
if (Props.ignoreSleepingEnemies && otherPawn.CurJobDef == JobDefOf.LayDown) continue;
|
||||
|
||||
// 检查是否为敌对关系
|
||||
if (!otherPawn.HostileTo(Sightstealer)) continue;
|
||||
|
||||
// 检查敌人类型过滤器(如果有)
|
||||
if (Props.enemyTypeFilter != null && Props.enemyTypeFilter.Count > 0)
|
||||
{
|
||||
if (!Props.enemyTypeFilter.Contains(otherPawn.def)) continue;
|
||||
}
|
||||
|
||||
// 关键修改:直接检查直线可见性,不使用距离限制
|
||||
if (GenSight.LineOfSight(Sightstealer.Position, otherPawn.Position, Sightstealer.Map))
|
||||
{
|
||||
enemiesInSight.Add(otherPawn);
|
||||
enemyInSight = true;
|
||||
|
||||
// 如果只需要知道是否有敌人,且已经找到一个,可以提前退出循环
|
||||
if (Props.minEnemiesToReveal <= 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果启用敌人检测后解除隐身,并且发现了足够数量的敌人
|
||||
if (enemyInSight && Props.revealOnEnemyInSight && enemiesInSight.Count >= Props.minEnemiesToReveal)
|
||||
{
|
||||
// 立即解除隐身
|
||||
Invisibility.BecomeVisible();
|
||||
lastDetectedTick = Find.TickManager.TicksGame;
|
||||
lastRevealedTick = Find.TickManager.TicksGame;
|
||||
|
||||
// 触发显现事件
|
||||
TrySendLetter("detected", enemiesInSight);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试发送信件
|
||||
/// </summary>
|
||||
private void TrySendLetter(string cause, List<Pawn> enemies = null)
|
||||
{
|
||||
// 检查是否应该发送信件
|
||||
if (!ShouldSendLetter())
|
||||
return;
|
||||
|
||||
// 发送信件
|
||||
SendLetter(cause, enemies);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否应该发送信件
|
||||
/// </summary>
|
||||
private bool ShouldSendLetter()
|
||||
{
|
||||
// 如果配置为不发信,直接返回false
|
||||
if (!Props.sendLetterOnReveal)
|
||||
return false;
|
||||
|
||||
// 检查发送间隔
|
||||
int currentTick = Find.TickManager.TicksGame;
|
||||
if (currentTick < lastLetterTick + Props.letterIntervalTicks)
|
||||
{
|
||||
// 还没到发送间隔
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Pawn是否非玩家控制
|
||||
if (Sightstealer.Faction == Faction.OfPlayer)
|
||||
{
|
||||
// 玩家控制的Pawn,不发送信件
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送信件
|
||||
/// </summary>
|
||||
private void SendLetter(string cause, List<Pawn> enemies = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
int currentTick = Find.TickManager.TicksGame;
|
||||
|
||||
// 获取信件标题和内容
|
||||
string title = Props.letterTitle;
|
||||
string text = Props.letterText;
|
||||
|
||||
// 如果标题或内容为空,使用默认值
|
||||
if (string.IsNullOrEmpty(title))
|
||||
title = "隐身单位现身";
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
string enemyInfo = "";
|
||||
if (enemies != null && enemies.Count > 0)
|
||||
{
|
||||
if (enemies.Count == 1)
|
||||
{
|
||||
enemyInfo = $"被 {enemies[0].LabelCap} 发现";
|
||||
}
|
||||
else
|
||||
{
|
||||
enemyInfo = $"被 {enemies.Count} 个敌人发现";
|
||||
}
|
||||
}
|
||||
else if (cause == "attack")
|
||||
{
|
||||
enemyInfo = "发动了攻击";
|
||||
}
|
||||
|
||||
text = $"{Sightstealer.LabelCap}({Sightstealer.Faction?.Name ?? "未知派系"})在 {Sightstealer.Map?.Parent?.LabelCap ?? "未知位置"} 现身了。\n\n{enemyInfo}\n位置:{Sightstealer.Position}";
|
||||
}
|
||||
|
||||
// 发送信件
|
||||
Letter letter = LetterMaker.MakeLetter(
|
||||
title,
|
||||
text,
|
||||
LetterDefOf.NeutralEvent,
|
||||
new LookTargets(Sightstealer)
|
||||
);
|
||||
|
||||
Find.LetterStack.ReceiveLetter(letter);
|
||||
|
||||
// 更新最后发信时间
|
||||
lastLetterTick = currentTick;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
WulaLog.Debug($"CompFighterInvisible: Error sending letter for {Sightstealer?.LabelCap}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// ... 其他方法保持不变 ...
|
||||
|
||||
/// <summary>
|
||||
/// 手动触发解除隐身(供外部调用)
|
||||
/// </summary>
|
||||
public void ForceReveal()
|
||||
{
|
||||
if (Invisibility == null) return;
|
||||
|
||||
Invisibility.BecomeVisible();
|
||||
lastDetectedTick = Find.TickManager.TicksGame;
|
||||
lastRevealedTick = Find.TickManager.TicksGame;
|
||||
|
||||
// 尝试发送信件
|
||||
TrySendLetter("manual");
|
||||
}
|
||||
|
||||
// ... 其他方法保持不变 ...
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_FighterInvisible : CompProperties
|
||||
{
|
||||
public float BaseVisibleRadius = 14f;
|
||||
public int UndetectedTimeout = 120;
|
||||
public int CheckDetectedIntervalTicks = 7;
|
||||
public float FirstDetectedRadius = 30f;
|
||||
public int RevealedLetterDelayTicks = 6;
|
||||
public int AmbushCallMTBTicks = 600;
|
||||
|
||||
// 修改:一个可定义的提供隐身的hediff
|
||||
public HediffDef InvisibilityDef;
|
||||
|
||||
// 隐身冷却
|
||||
public int stealthCooldownTicks = 1200;
|
||||
|
||||
// 新增:是否在视线内出现敌人时解除隐身
|
||||
public bool revealOnEnemyInSight = false;
|
||||
|
||||
// 新增:解除隐身的检测半径(默认为FirstDetectedRadius)
|
||||
public float revealDetectionRadius = 500f;
|
||||
|
||||
// 新增:是否显示解除隐身效果
|
||||
public bool showRevealEffect = true;
|
||||
|
||||
// 新增:解除隐身时的声音
|
||||
public SoundDef revealSound;
|
||||
|
||||
// 新增:是否发送解除隐身消息
|
||||
public bool sendRevealMessage = false;
|
||||
|
||||
// 新增:解除隐身的最小敌人数量(默认为1)
|
||||
public int minEnemiesToReveal = 1;
|
||||
|
||||
// 新增:敌人类型过滤器(空表示所有敌人)
|
||||
public List<ThingDef> enemyTypeFilter;
|
||||
|
||||
// 新增:是否只在战斗状态时检查敌人
|
||||
public bool onlyCheckInCombat = false;
|
||||
|
||||
// 新增:是否忽略某些状态的敌人(如倒地、死亡等)
|
||||
public bool ignoreDownedEnemies = true;
|
||||
public bool ignoreSleepingEnemies = false;
|
||||
|
||||
|
||||
// 新增:简化发信配置
|
||||
public bool sendLetterOnReveal = false;
|
||||
public string letterTitle = "";
|
||||
public string letterText = "";
|
||||
public int letterIntervalTicks = 1200;
|
||||
|
||||
public CompProperties_FighterInvisible()
|
||||
{
|
||||
compClass = typeof(CompFighterInvisible);
|
||||
}
|
||||
|
||||
public override IEnumerable<string> ConfigErrors(ThingDef parentDef)
|
||||
{
|
||||
foreach (string error in base.ConfigErrors(parentDef))
|
||||
{
|
||||
yield return error;
|
||||
}
|
||||
|
||||
if (InvisibilityDef == null)
|
||||
{
|
||||
yield return "InvisibilityDef is not defined for CompProperties_FighterInvisible";
|
||||
}
|
||||
|
||||
if (revealDetectionRadius <= 0)
|
||||
{
|
||||
revealDetectionRadius = FirstDetectedRadius;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ namespace WulaFallenEmpire
|
||||
var pilotKind = Props.SelectRandomPilotKind();
|
||||
if (pilotKind == null)
|
||||
{
|
||||
Log.Warning($"[DD] No valid pilot kind found");
|
||||
Log.Warning($"[WULA] No valid pilot kind found");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"[DD] Cannot add pilot {pilot.LabelShortCap} to mech");
|
||||
Log.Warning($"[WULA] Cannot add pilot {pilot.LabelShortCap} to mech");
|
||||
// 清理生成的pawn
|
||||
pilot.Destroy();
|
||||
return false;
|
||||
@@ -159,7 +159,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"[DD] Error generating default pilot: {ex}");
|
||||
Log.Error($"[WULA] Error generating default pilot: {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
using RimWorld;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompHediffGiver : ThingComp
|
||||
{
|
||||
private bool hediffsApplied = false; // 新增:标记是否已经应用过hediff
|
||||
|
||||
public CompProperties_HediffGiver Props => (CompProperties_HediffGiver)this.props;
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
base.PostSpawnSetup(respawningAfterLoad);
|
||||
|
||||
// 只有当thing是pawn时才添加hediff
|
||||
if (this.parent is Pawn pawn)
|
||||
{
|
||||
// 新增:检查是否已经应用过hediff,或者是否是读档
|
||||
if (!hediffsApplied && !respawningAfterLoad)
|
||||
{
|
||||
AddHediffsToPawn(pawn);
|
||||
hediffsApplied = true; // 标记为已应用
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddHediffsToPawn(Pawn pawn)
|
||||
{
|
||||
// 检查是否有hediff列表
|
||||
if (Props.hediffs == null || Props.hediffs.Count == 0)
|
||||
return;
|
||||
|
||||
// 检查概率
|
||||
if (Props.addChance < 1.0f && Rand.Value > Props.addChance)
|
||||
return;
|
||||
|
||||
// 为每个hediff添加到pawn
|
||||
foreach (HediffDef hediffDef in Props.hediffs)
|
||||
{
|
||||
// 检查是否允许重复添加
|
||||
if (!Props.allowDuplicates && pawn.health.hediffSet.HasHediff(hediffDef))
|
||||
continue;
|
||||
|
||||
// 添加hediff
|
||||
pawn.health.AddHediff(hediffDef);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:序列化hediffsApplied标记
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref hediffsApplied, "hediffsApplied", false);
|
||||
}
|
||||
|
||||
// 新增:调试方法,用于手动触发hediff添加(仅开发模式)
|
||||
public void DebugApplyHediffs()
|
||||
{
|
||||
if (this.parent is Pawn pawn && !hediffsApplied)
|
||||
{
|
||||
AddHediffsToPawn(pawn);
|
||||
hediffsApplied = true;
|
||||
WulaLog.Debug($"Debug: Applied hediffs to {pawn.Label}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_HediffGiver : CompProperties
|
||||
{
|
||||
// 要添加的hediff列表
|
||||
public List<HediffDef> hediffs;
|
||||
|
||||
// 添加hediff的概率(0-1之间)
|
||||
public float addChance = 1.0f;
|
||||
|
||||
// 是否允许重复添加相同的hediff
|
||||
public bool allowDuplicates = false;
|
||||
|
||||
public CompProperties_HediffGiver()
|
||||
{
|
||||
this.compClass = typeof(CompHediffGiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,855 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
using Verse.Sound;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// 高速移动撞击组件
|
||||
/// </summary>
|
||||
public class CompHighSpeedCollision : ThingComp
|
||||
{
|
||||
// === 运行时状态 ===
|
||||
private enum SpeedStage
|
||||
{
|
||||
Stage0, // 0阶段:不移动
|
||||
Stage1, // 1阶段:低速碰撞
|
||||
Stage2 // 2阶段:高速击飞
|
||||
}
|
||||
|
||||
private SpeedStage currentStage = SpeedStage.Stage0;
|
||||
private int stageTransitionCooldown = 0;
|
||||
|
||||
// 用于计算速度的帧历史
|
||||
private Queue<float> speedHistory = new Queue<float>();
|
||||
private IntVec3 lastPosition = IntVec3.Invalid;
|
||||
private int lastPositionTick = -1;
|
||||
|
||||
// 已处理的敌人记录(避免同一帧重复处理)
|
||||
private HashSet<Pawn> processedPawnsThisTick = new HashSet<Pawn>();
|
||||
|
||||
// === 缓存 ===
|
||||
private CellRect collisionAreaCache = default;
|
||||
private int lastAreaRecalculationTick = -1;
|
||||
|
||||
public CompProperties_HighSpeedCollision Props => (CompProperties_HighSpeedCollision)props;
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
base.PostSpawnSetup(respawningAfterLoad);
|
||||
|
||||
// 初始化速度历史
|
||||
speedHistory.Clear();
|
||||
for (int i = 0; i < Props.speedHistoryFrameCount; i++)
|
||||
{
|
||||
speedHistory.Enqueue(0f);
|
||||
}
|
||||
|
||||
lastPosition = parent.Position;
|
||||
lastPositionTick = Find.TickManager.TicksGame;
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
if (!parent.Spawned || parent.Destroyed)
|
||||
return;
|
||||
|
||||
Pawn pawn = parent as Pawn;
|
||||
if (pawn == null || pawn.Dead || pawn.Downed)
|
||||
return;
|
||||
|
||||
// 检查是否死亡或不能移动
|
||||
if (!CanMove(pawn))
|
||||
{
|
||||
ResetToStage0();
|
||||
return;
|
||||
}
|
||||
|
||||
// 每帧更新
|
||||
ProcessFrame(pawn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理每帧逻辑
|
||||
/// </summary>
|
||||
private void ProcessFrame(Pawn pawn)
|
||||
{
|
||||
int currentTick = Find.TickManager.TicksGame;
|
||||
|
||||
// 1. 计算当前速度
|
||||
float currentSpeed = CalculateCurrentSpeed(pawn, currentTick);
|
||||
|
||||
// 2. 更新速度历史
|
||||
UpdateSpeedHistory(currentSpeed);
|
||||
|
||||
// 3. 计算平均速度
|
||||
float averageSpeed = GetAverageSpeed();
|
||||
|
||||
// 4. 确定阶段
|
||||
DetermineSpeedStage(averageSpeed);
|
||||
|
||||
// 5. 根据阶段应用效果
|
||||
ApplyStageEffects(pawn);
|
||||
|
||||
// 6. 清理每帧记录
|
||||
processedPawnsThisTick.Clear();
|
||||
|
||||
// 7. 更新冷却
|
||||
if (stageTransitionCooldown > 0)
|
||||
stageTransitionCooldown--;
|
||||
|
||||
// 8. 调试绘制
|
||||
if (Props.enableDebugVisuals && DebugSettings.godMode)
|
||||
DrawDebugVisuals(pawn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算当前速度(每秒格数)
|
||||
/// </summary>
|
||||
private float CalculateCurrentSpeed(Pawn pawn, int currentTick)
|
||||
{
|
||||
// 如果没有上次位置记录,无法计算速度
|
||||
if (lastPositionTick < 0 || lastPosition == IntVec3.Invalid)
|
||||
{
|
||||
lastPosition = pawn.Position;
|
||||
lastPositionTick = currentTick;
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// 计算时间差(秒)
|
||||
float timeDelta = (currentTick - lastPositionTick) / 60f;
|
||||
if (timeDelta <= 0f)
|
||||
return 0f;
|
||||
|
||||
// 计算距离(格数)
|
||||
float distance = pawn.Position.DistanceTo(lastPosition);
|
||||
|
||||
// 计算速度(格/秒)
|
||||
float speed = distance / timeDelta;
|
||||
|
||||
// 更新记录
|
||||
lastPosition = pawn.Position;
|
||||
lastPositionTick = currentTick;
|
||||
|
||||
return speed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新速度历史
|
||||
/// </summary>
|
||||
private void UpdateSpeedHistory(float currentSpeed)
|
||||
{
|
||||
speedHistory.Enqueue(currentSpeed);
|
||||
while (speedHistory.Count > Props.speedHistoryFrameCount)
|
||||
{
|
||||
speedHistory.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取平均速度
|
||||
/// </summary>
|
||||
private float GetAverageSpeed()
|
||||
{
|
||||
if (speedHistory.Count == 0)
|
||||
return 0f;
|
||||
|
||||
float sum = 0f;
|
||||
foreach (float speed in speedHistory)
|
||||
{
|
||||
sum += speed;
|
||||
}
|
||||
|
||||
return sum / speedHistory.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确定速度阶段
|
||||
/// </summary>
|
||||
private void DetermineSpeedStage(float averageSpeed)
|
||||
{
|
||||
// 如果有冷却,保持当前阶段
|
||||
if (stageTransitionCooldown > 0)
|
||||
return;
|
||||
|
||||
SpeedStage newStage;
|
||||
|
||||
if (averageSpeed <= Props.minSpeedForStage1)
|
||||
{
|
||||
newStage = SpeedStage.Stage0;
|
||||
}
|
||||
else if (averageSpeed >= Props.minSpeedForStage2)
|
||||
{
|
||||
newStage = SpeedStage.Stage2;
|
||||
}
|
||||
else
|
||||
{
|
||||
newStage = SpeedStage.Stage1;
|
||||
}
|
||||
|
||||
// 阶段变化时设置冷却
|
||||
if (newStage != currentStage)
|
||||
{
|
||||
currentStage = newStage;
|
||||
stageTransitionCooldown = Props.stageTransitionCooldownTicks;
|
||||
|
||||
if (Props.enableDebugLogging)
|
||||
{
|
||||
Log.Message($"[HighSpeedCollision] {parent.Label} transitioned to Stage {(int)currentStage} " +
|
||||
$"at speed {averageSpeed:F2} cells/sec");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用阶段效果
|
||||
/// </summary>
|
||||
private void ApplyStageEffects(Pawn pawn)
|
||||
{
|
||||
if (currentStage == SpeedStage.Stage0)
|
||||
return;
|
||||
|
||||
// 获取碰撞区域内的所有敌人
|
||||
List<Pawn> enemiesInArea = GetEnemiesInCollisionArea(pawn);
|
||||
|
||||
foreach (Pawn enemy in enemiesInArea)
|
||||
{
|
||||
if (enemy == null || enemy.Destroyed || enemy.Dead || processedPawnsThisTick.Contains(enemy))
|
||||
continue;
|
||||
|
||||
switch (currentStage)
|
||||
{
|
||||
case SpeedStage.Stage1:
|
||||
ApplyStage1Effects(pawn, enemy);
|
||||
break;
|
||||
case SpeedStage.Stage2:
|
||||
ApplyStage2Effects(pawn, enemy);
|
||||
break;
|
||||
}
|
||||
|
||||
processedPawnsThisTick.Add(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用阶段1效果(伤害+hediff)
|
||||
/// </summary>
|
||||
private void ApplyStage1Effects(Pawn attacker, Pawn target)
|
||||
{
|
||||
// 检查目标是否已有hediff
|
||||
bool alreadyHasHediff = target.health.hediffSet.HasHediff(Props.stage1Hediff);
|
||||
|
||||
// 如果已有hediff,不造成伤害
|
||||
if (alreadyHasHediff && Props.stage1HediffPreventsDamage)
|
||||
return;
|
||||
|
||||
// 造成伤害
|
||||
if (Props.stage1DamageAmount > 0f)
|
||||
{
|
||||
ApplyDamage(attacker, target, Props.stage1DamageAmount, Props.stage1DamageDef);
|
||||
}
|
||||
|
||||
// 应用hediff
|
||||
if (Props.stage1Hediff != null)
|
||||
{
|
||||
Hediff hediff = HediffMaker.MakeHediff(Props.stage1Hediff, target);
|
||||
if (Props.stage1HediffDurationTicks > 0)
|
||||
{
|
||||
hediff.Severity = 1f;
|
||||
hediff.TryGetComp<HediffComp_Disappears>()?.CompPostMake();
|
||||
}
|
||||
target.health.AddHediff(hediff);
|
||||
}
|
||||
|
||||
// 播放效果
|
||||
PlayStage1Effects(attacker, target);
|
||||
|
||||
if (Props.enableDebugLogging)
|
||||
{
|
||||
Log.Message($"[HighSpeedCollision] Stage1: {attacker.Label} -> {target.Label}, " +
|
||||
$"Damage: {Props.stage1DamageAmount}, Hediff: {Props.stage1Hediff?.defName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用阶段2效果(伤害+击飞)
|
||||
/// </summary>
|
||||
private void ApplyStage2Effects(Pawn attacker, Pawn target)
|
||||
{
|
||||
// 造成伤害
|
||||
if (Props.stage2DamageAmount > 0f)
|
||||
{
|
||||
ApplyDamage(attacker, target, Props.stage2DamageAmount, Props.stage2DamageDef);
|
||||
}
|
||||
|
||||
// 执行击飞
|
||||
PerformKnockback(attacker, target);
|
||||
|
||||
// 播放效果
|
||||
PlayStage2Effects(attacker, target);
|
||||
|
||||
if (Props.enableDebugLogging)
|
||||
{
|
||||
Log.Message($"[HighSpeedCollision] Stage2: {attacker.Label} -> {target.Label}, " +
|
||||
$"Damage: {Props.stage2DamageAmount}, Knockback");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行击飞(参考CompAbilityEffect_FanShapedStunKnockback)
|
||||
/// </summary>
|
||||
private void PerformKnockback(Pawn attacker, Pawn target)
|
||||
{
|
||||
if (target == null || target.Destroyed || target.Dead || attacker.Map == null)
|
||||
return;
|
||||
|
||||
// 计算击飞方向(从攻击者指向目标)
|
||||
IntVec3 knockbackDirection = CalculateKnockbackDirection(attacker, target.Position);
|
||||
|
||||
// 寻找击飞位置
|
||||
IntVec3 knockbackDestination = FindKnockbackDestination(attacker, target, knockbackDirection);
|
||||
|
||||
// 如果找到有效位置,执行击飞
|
||||
if (knockbackDestination.IsValid && knockbackDestination != target.Position)
|
||||
{
|
||||
CreateKnockbackFlyer(attacker, target, knockbackDestination);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算击飞方向
|
||||
/// </summary>
|
||||
private IntVec3 CalculateKnockbackDirection(Pawn attacker, IntVec3 targetPosition)
|
||||
{
|
||||
IntVec3 direction = targetPosition - attacker.Position;
|
||||
|
||||
// 标准化方向
|
||||
if (direction.x != 0 || direction.z != 0)
|
||||
{
|
||||
if (Mathf.Abs(direction.x) > Mathf.Abs(direction.z))
|
||||
{
|
||||
return new IntVec3(Mathf.Sign(direction.x) > 0 ? 1 : -1, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new IntVec3(0, 0, Mathf.Sign(direction.z) > 0 ? 1 : -1);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果攻击者和目标在同一位置,使用随机方向
|
||||
return new IntVec3(Rand.Value > 0.5f ? 1 : -1, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 寻找击飞位置
|
||||
/// </summary>
|
||||
private IntVec3 FindKnockbackDestination(Pawn attacker, Pawn target, IntVec3 direction)
|
||||
{
|
||||
Map map = attacker.Map;
|
||||
IntVec3 currentPos = target.Position;
|
||||
|
||||
// 从最大距离开始向回找
|
||||
for (int distance = Props.stage2KnockbackDistance; distance >= 1; distance--)
|
||||
{
|
||||
IntVec3 testPos = currentPos + (direction * distance);
|
||||
|
||||
if (!IsValidKnockbackDestination(testPos, map, target, attacker))
|
||||
continue;
|
||||
|
||||
return testPos;
|
||||
}
|
||||
|
||||
return currentPos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查击飞位置是否有效
|
||||
/// </summary>
|
||||
private bool IsValidKnockbackDestination(IntVec3 destination, Map map, Pawn victim, Pawn attacker)
|
||||
{
|
||||
if (!destination.IsValid || !destination.InBounds(map))
|
||||
return false;
|
||||
|
||||
if (!destination.Standable(map))
|
||||
return false;
|
||||
|
||||
// 检查是否有其他pawn
|
||||
Pawn existingPawn = destination.GetFirstPawn(map);
|
||||
if (existingPawn != null && existingPawn != victim)
|
||||
return false;
|
||||
|
||||
// 检查视线
|
||||
if (Props.requireLineOfSightForKnockback && !GenSight.LineOfSight(victim.Position, destination, map))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建击飞飞行器
|
||||
/// </summary>
|
||||
private void CreateKnockbackFlyer(Pawn attacker, Pawn target, IntVec3 destination)
|
||||
{
|
||||
try
|
||||
{
|
||||
Map map = attacker.Map;
|
||||
|
||||
// 使用自定义飞行器或默认飞行器
|
||||
ThingDef flyerDef = Props.knockbackFlyerDef ?? ThingDefOf.PawnFlyer;
|
||||
|
||||
// 创建飞行器
|
||||
PawnFlyer flyer = PawnFlyer.MakeFlyer(
|
||||
flyerDef,
|
||||
target,
|
||||
destination,
|
||||
Props.flightEffecterDef,
|
||||
Props.landingSound,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
new LocalTargetInfo(destination)
|
||||
);
|
||||
|
||||
if (flyer != null)
|
||||
{
|
||||
GenSpawn.Spawn(flyer, destination, map);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[HighSpeedCollision] Exception creating PawnFlyer: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用伤害
|
||||
/// </summary>
|
||||
private void ApplyDamage(Pawn attacker, Pawn target, float amount, DamageDef damageDef)
|
||||
{
|
||||
if (amount <= 0f || damageDef == null)
|
||||
return;
|
||||
|
||||
DamageInfo damageInfo = new DamageInfo(
|
||||
damageDef,
|
||||
amount,
|
||||
Props.armorPenetration,
|
||||
-1f,
|
||||
attacker,
|
||||
null
|
||||
);
|
||||
|
||||
target.TakeDamage(damageInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放阶段1效果
|
||||
/// </summary>
|
||||
private void PlayStage1Effects(Pawn attacker, Pawn target)
|
||||
{
|
||||
if (Props.stage1Effecter != null && attacker.Map != null)
|
||||
{
|
||||
Effecter effect = Props.stage1Effecter.Spawn();
|
||||
effect.Trigger(new TargetInfo(attacker.Position, attacker.Map),
|
||||
new TargetInfo(target.Position, attacker.Map));
|
||||
effect.Cleanup();
|
||||
}
|
||||
|
||||
if (Props.stage1Sound != null && attacker.Map != null)
|
||||
{
|
||||
Props.stage1Sound.PlayOneShot(new TargetInfo(target.Position, attacker.Map));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放阶段2效果
|
||||
/// </summary>
|
||||
private void PlayStage2Effects(Pawn attacker, Pawn target)
|
||||
{
|
||||
if (Props.stage2Effecter != null && attacker.Map != null)
|
||||
{
|
||||
Effecter effect = Props.stage2Effecter.Spawn();
|
||||
effect.Trigger(new TargetInfo(attacker.Position, attacker.Map),
|
||||
new TargetInfo(target.Position, attacker.Map));
|
||||
effect.Cleanup();
|
||||
}
|
||||
|
||||
if (Props.stage2Sound != null && attacker.Map != null)
|
||||
{
|
||||
Props.stage2Sound.PlayOneShot(new TargetInfo(target.Position, attacker.Map));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取碰撞区域内的所有敌人
|
||||
/// </summary>
|
||||
private List<Pawn> GetEnemiesInCollisionArea(Pawn pawn)
|
||||
{
|
||||
List<Pawn> enemies = new List<Pawn>();
|
||||
|
||||
// 获取碰撞区域
|
||||
CellRect collisionArea = GetCollisionArea(pawn);
|
||||
|
||||
// 检查区域内的每个单元格
|
||||
foreach (IntVec3 cell in collisionArea)
|
||||
{
|
||||
if (!cell.InBounds(pawn.Map))
|
||||
continue;
|
||||
|
||||
// 获取单元格内的所有pawn
|
||||
List<Thing> things = cell.GetThingList(pawn.Map);
|
||||
foreach (Thing thing in things)
|
||||
{
|
||||
if (thing is Pawn otherPawn && otherPawn != pawn)
|
||||
{
|
||||
// 检查是否为敌人
|
||||
if (IsValidTarget(pawn, otherPawn))
|
||||
{
|
||||
enemies.Add(otherPawn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return enemies;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取碰撞区域
|
||||
/// </summary>
|
||||
private CellRect GetCollisionArea(Pawn pawn)
|
||||
{
|
||||
int currentTick = Find.TickManager.TicksGame;
|
||||
|
||||
// 每10帧重新计算一次区域,或当位置变化时
|
||||
if (currentTick - lastAreaRecalculationTick > 10 ||
|
||||
pawn.Position != collisionAreaCache.CenterCell)
|
||||
{
|
||||
int radius = Props.collisionAreaRadius;
|
||||
IntVec3 center = pawn.Position;
|
||||
|
||||
collisionAreaCache = new CellRect(
|
||||
center.x - radius,
|
||||
center.z - radius,
|
||||
radius * 2 + 1,
|
||||
radius * 2 + 1
|
||||
);
|
||||
|
||||
collisionAreaCache.ClipInsideMap(pawn.Map);
|
||||
lastAreaRecalculationTick = currentTick;
|
||||
}
|
||||
|
||||
return collisionAreaCache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否是有效目标
|
||||
/// </summary>
|
||||
private bool IsValidTarget(Pawn attacker, Pawn target)
|
||||
{
|
||||
if (target == null || target.Destroyed || target.Dead)
|
||||
return false;
|
||||
|
||||
// 检查是否为敌人
|
||||
if (Props.onlyAffectEnemies && !target.HostileTo(attacker))
|
||||
return false;
|
||||
|
||||
// 检查是否排除友方
|
||||
if (Props.excludeAlliedPawns && target.Faction == attacker.Faction)
|
||||
return false;
|
||||
|
||||
// 检查是否排除中立
|
||||
if (Props.excludeNeutralPawns && !target.HostileTo(attacker))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查Pawn是否可以移动
|
||||
/// </summary>
|
||||
private bool CanMove(Pawn pawn)
|
||||
{
|
||||
if (pawn.Downed || pawn.Dead || pawn.InMentalState)
|
||||
return false;
|
||||
|
||||
if (pawn.stances?.stunner?.Stunned ?? false)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置到阶段0
|
||||
/// </summary>
|
||||
private void ResetToStage0()
|
||||
{
|
||||
currentStage = SpeedStage.Stage0;
|
||||
|
||||
// 清空速度历史
|
||||
speedHistory.Clear();
|
||||
for (int i = 0; i < Props.speedHistoryFrameCount; i++)
|
||||
{
|
||||
speedHistory.Enqueue(0f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制调试视觉效果
|
||||
/// </summary>
|
||||
private void DrawDebugVisuals(Pawn pawn)
|
||||
{
|
||||
if (!pawn.Spawned)
|
||||
return;
|
||||
|
||||
// 绘制碰撞区域
|
||||
CellRect area = GetCollisionArea(pawn);
|
||||
GenDraw.DrawFieldEdges(area.Cells.ToList(), GetStageColor(currentStage));
|
||||
|
||||
// 绘制速度指示器
|
||||
float averageSpeed = GetAverageSpeed();
|
||||
string speedText = $"Speed: {averageSpeed:F1} cells/sec\nStage: {(int)currentStage}";
|
||||
|
||||
Vector3 drawPos = pawn.DrawPos + new Vector3(0, 0, 1f);
|
||||
GenMapUI.DrawText(drawPos, speedText, GetStageColor(currentStage));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取阶段颜色
|
||||
/// </summary>
|
||||
private Color GetStageColor(SpeedStage stage)
|
||||
{
|
||||
switch (stage)
|
||||
{
|
||||
case SpeedStage.Stage0: return Color.gray;
|
||||
case SpeedStage.Stage1: return Color.yellow;
|
||||
case SpeedStage.Stage2: return Color.red;
|
||||
default: return Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取调试信息
|
||||
/// </summary>
|
||||
public string GetDebugInfo()
|
||||
{
|
||||
float averageSpeed = GetAverageSpeed();
|
||||
|
||||
return $"HighSpeedCollision Debug Info:\n" +
|
||||
$"Current Stage: {(int)currentStage}\n" +
|
||||
$"Average Speed: {averageSpeed:F2} cells/sec\n" +
|
||||
$"Stage 1 Threshold: {Props.minSpeedForStage1:F2}\n" +
|
||||
$"Stage 2 Threshold: {Props.minSpeedForStage2:F2}\n" +
|
||||
$"Speed History: {speedHistory.Count} frames\n" +
|
||||
$"Stage Cooldown: {stageTransitionCooldown} ticks";
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
|
||||
Scribe_Values.Look(ref currentStage, "currentStage", SpeedStage.Stage0);
|
||||
Scribe_Values.Look(ref stageTransitionCooldown, "stageTransitionCooldown", 0);
|
||||
Scribe_Values.Look(ref lastPosition, "lastPosition", IntVec3.Invalid);
|
||||
Scribe_Values.Look(ref lastPositionTick, "lastPositionTick", -1);
|
||||
|
||||
// 保存速度历史
|
||||
if (Scribe.mode == LoadSaveMode.Saving)
|
||||
{
|
||||
List<float> speedList = speedHistory.ToList();
|
||||
Scribe_Collections.Look(ref speedList, "speedHistory", LookMode.Value);
|
||||
}
|
||||
else if (Scribe.mode == LoadSaveMode.LoadingVars)
|
||||
{
|
||||
List<float> speedList = null;
|
||||
Scribe_Collections.Look(ref speedList, "speedHistory", LookMode.Value);
|
||||
|
||||
speedHistory.Clear();
|
||||
if (speedList != null)
|
||||
{
|
||||
foreach (float speed in speedList)
|
||||
{
|
||||
speedHistory.Enqueue(speed);
|
||||
}
|
||||
}
|
||||
|
||||
// 确保有足够的历史数据
|
||||
while (speedHistory.Count < Props.speedHistoryFrameCount)
|
||||
{
|
||||
speedHistory.Enqueue(0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高速移动撞击组件属性
|
||||
/// </summary>
|
||||
public class CompProperties_HighSpeedCollision : CompProperties
|
||||
{
|
||||
// === 速度阈值配置 ===
|
||||
|
||||
/// <summary>
|
||||
/// 进入阶段1所需的最小速度(格/秒)
|
||||
/// </summary>
|
||||
public float minSpeedForStage1 = 3f;
|
||||
|
||||
/// <summary>
|
||||
/// 进入阶段2所需的最小速度(格/秒)
|
||||
/// </summary>
|
||||
public float minSpeedForStage2 = 6f;
|
||||
|
||||
/// <summary>
|
||||
/// 速度历史帧数(用于计算平均速度)
|
||||
/// </summary>
|
||||
public int speedHistoryFrameCount = 10;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段转换冷却时间(tick)
|
||||
/// </summary>
|
||||
public int stageTransitionCooldownTicks = 5;
|
||||
|
||||
// === 碰撞区域配置 ===
|
||||
|
||||
/// <summary>
|
||||
/// 碰撞区域半径(以pawn为中心的正方形)
|
||||
/// </summary>
|
||||
public int collisionAreaRadius = 1;
|
||||
|
||||
// === 目标过滤 ===
|
||||
|
||||
/// <summary>
|
||||
/// 只影响敌人
|
||||
/// </summary>
|
||||
public bool onlyAffectEnemies = true;
|
||||
|
||||
/// <summary>
|
||||
/// 排除友方单位
|
||||
/// </summary>
|
||||
public bool excludeAlliedPawns = true;
|
||||
|
||||
/// <summary>
|
||||
/// 排除中立单位
|
||||
/// </summary>
|
||||
public bool excludeNeutralPawns = false;
|
||||
|
||||
// === 阶段1效果配置 ===
|
||||
|
||||
/// <summary>
|
||||
/// 阶段1伤害类型
|
||||
/// </summary>
|
||||
public DamageDef stage1DamageDef = DamageDefOf.Blunt;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段1伤害量
|
||||
/// </summary>
|
||||
public float stage1DamageAmount = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段1护甲穿透
|
||||
/// </summary>
|
||||
public float armorPenetration = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段1应用的hediff
|
||||
/// </summary>
|
||||
public HediffDef stage1Hediff;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段1hediff持续时间(tick)
|
||||
/// </summary>
|
||||
public int stage1HediffDurationTicks = 60;
|
||||
|
||||
/// <summary>
|
||||
/// 拥有hediff的目标是否免疫伤害
|
||||
/// </summary>
|
||||
public bool stage1HediffPreventsDamage = true;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段1效果器
|
||||
/// </summary>
|
||||
public EffecterDef stage1Effecter;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段1音效
|
||||
/// </summary>
|
||||
public SoundDef stage1Sound;
|
||||
|
||||
// === 阶段2效果配置 ===
|
||||
|
||||
/// <summary>
|
||||
/// 阶段2伤害类型
|
||||
/// </summary>
|
||||
public DamageDef stage2DamageDef = DamageDefOf.Blunt;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段2伤害量
|
||||
/// </summary>
|
||||
public float stage2DamageAmount = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段2击退距离
|
||||
/// </summary>
|
||||
public int stage2KnockbackDistance = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 击退是否需要视线
|
||||
/// </summary>
|
||||
public bool requireLineOfSightForKnockback = true;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段2效果器
|
||||
/// </summary>
|
||||
public EffecterDef stage2Effecter;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段2音效
|
||||
/// </summary>
|
||||
public SoundDef stage2Sound;
|
||||
|
||||
// === 击飞配置 ===
|
||||
|
||||
/// <summary>
|
||||
/// 击退飞行器定义
|
||||
/// </summary>
|
||||
public ThingDef knockbackFlyerDef;
|
||||
|
||||
/// <summary>
|
||||
/// 飞行效果器
|
||||
/// </summary>
|
||||
public EffecterDef flightEffecterDef;
|
||||
|
||||
/// <summary>
|
||||
/// 落地音效
|
||||
/// </summary>
|
||||
public SoundDef landingSound;
|
||||
|
||||
// === 调试配置 ===
|
||||
|
||||
/// <summary>
|
||||
/// 启用调试日志
|
||||
/// </summary>
|
||||
public bool enableDebugLogging = false;
|
||||
|
||||
/// <summary>
|
||||
/// 启用调试视觉效果
|
||||
/// </summary>
|
||||
public bool enableDebugVisuals = false;
|
||||
|
||||
/// <summary>
|
||||
/// 绘制速度历史图
|
||||
/// </summary>
|
||||
public bool debugDrawSpeedHistory = false;
|
||||
|
||||
public CompProperties_HighSpeedCollision()
|
||||
{
|
||||
compClass = typeof(CompHighSpeedCollision);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
// File: CompMechArmor.cs
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.Sound;
|
||||
using static RimWorld.MechClusterSketch;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// 机甲装甲组件:提供基于装甲值的伤害减免系统
|
||||
/// </summary>
|
||||
public class CompMechArmor : ThingComp
|
||||
{
|
||||
public CompProperties_MechArmor Props =>
|
||||
(CompProperties_MechArmor)props;
|
||||
|
||||
private static StatDef DD_MechArmorDef = null;
|
||||
|
||||
// 当前装甲值(从Stat获取)
|
||||
public float CurrentArmor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (DD_MechArmorDef == null)
|
||||
{
|
||||
DD_MechArmorDef = StatDef.Named("DD_MechArmor");
|
||||
if (DD_MechArmorDef == null)
|
||||
{
|
||||
Log.Warning("[DD] DD_MechArmor stat definition not found!");
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
return parent.GetStatValue(DD_MechArmorDef);
|
||||
}
|
||||
}
|
||||
|
||||
// 调试信息
|
||||
private int blockedHits = 0;
|
||||
private int totalHits = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 检查伤害是否被装甲抵消
|
||||
/// </summary>
|
||||
/// <param name="dinfo">伤害信息</param>
|
||||
/// <returns>true=伤害被抵消,false=伤害有效</returns>
|
||||
public bool TryBlockDamage(ref DamageInfo dinfo)
|
||||
{
|
||||
totalHits++;
|
||||
|
||||
// 获取穿甲率(如果没有则为0)
|
||||
float armorPenetration = dinfo.ArmorPenetrationInt;
|
||||
|
||||
// 计算穿甲伤害
|
||||
float armorPiercingDamage = dinfo.Amount * armorPenetration;
|
||||
|
||||
// 获取当前装甲值
|
||||
float currentArmor = CurrentArmor;
|
||||
|
||||
// 检查是否应该被装甲抵消
|
||||
bool shouldBlock = armorPiercingDamage < currentArmor;
|
||||
|
||||
if (shouldBlock)
|
||||
{
|
||||
blockedHits++;
|
||||
|
||||
// 可选:触发视觉效果
|
||||
if (Props.showBlockEffect && parent.Spawned)
|
||||
{
|
||||
ShowBlockEffect(dinfo);
|
||||
}
|
||||
|
||||
// 可选:播放音效
|
||||
if (Props.soundOnBlock != null && parent.Spawned)
|
||||
{
|
||||
Props.soundOnBlock.PlayOneShot(parent);
|
||||
}
|
||||
}
|
||||
|
||||
// 调试日志
|
||||
if (Props.debugLogging && parent.Spawned)
|
||||
{
|
||||
Log.Message($"[DD Armor] {parent.LabelShort}: " +
|
||||
$"Damage={dinfo.Amount}, " +
|
||||
$"Penetration={armorPenetration:P0}, " +
|
||||
$"PierceDamage={armorPiercingDamage:F1}, " +
|
||||
$"Armor={currentArmor:F1}, " +
|
||||
$"Blocked={shouldBlock}");
|
||||
}
|
||||
|
||||
return shouldBlock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示阻挡效果
|
||||
/// </summary>
|
||||
private void ShowBlockEffect(DamageInfo dinfo)
|
||||
{
|
||||
MoteMaker.ThrowText(parent.DrawPos, parent.Map, "DD_BlockByMechArmor".Translate(), Color.white, 3.5f);
|
||||
// 创建火花或特效
|
||||
if (Props.blockEffectMote != null)
|
||||
{
|
||||
MoteMaker.MakeStaticMote(
|
||||
parent.DrawPos + new Vector3(0, 0, 0.5f),
|
||||
parent.Map,
|
||||
Props.blockEffectMote,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取装甲信息(用于调试)
|
||||
/// </summary>
|
||||
public string GetArmorInfo()
|
||||
{
|
||||
return $"<b>{parent.LabelShort}的装甲系统</b>\n" +
|
||||
$"当前装甲值: {CurrentArmor:F1}\n" +
|
||||
$"阻挡规则: 穿甲伤害 < 装甲值\n" +
|
||||
$"统计: 已阻挡 {blockedHits}/{totalHits} 次攻击\n" +
|
||||
$"阻挡率: {(totalHits > 0 ? (float)blockedHits / totalHits * 100 : 0):F1}%";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取调试按钮
|
||||
/// </summary>
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
foreach (var gizmo in base.CompGetGizmosExtra())
|
||||
{
|
||||
yield return gizmo;
|
||||
}
|
||||
|
||||
// 在开发模式下显示装甲信息
|
||||
if (DebugSettings.ShowDevGizmos)
|
||||
{
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "DEBUG: 装甲信息",
|
||||
defaultDesc = GetArmorInfo(),
|
||||
//icon = TexCommand.Shield,
|
||||
action = () =>
|
||||
{
|
||||
Find.WindowStack.Add(new Dialog_MessageBox(
|
||||
GetArmorInfo(),
|
||||
"关闭",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"机甲装甲信息"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "DEBUG: 重置统计",
|
||||
defaultDesc = "重置阻挡统计计数器",
|
||||
//icon = TexCommand.Clear,
|
||||
action = () =>
|
||||
{
|
||||
blockedHits = 0;
|
||||
totalHits = 0;
|
||||
Messages.Message("装甲统计已重置", MessageTypeDefOf.TaskCompletion);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref blockedHits, "blockedHits", 0);
|
||||
Scribe_Values.Look(ref totalHits, "totalHits", 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 机甲装甲组件属性
|
||||
/// </summary>
|
||||
public class CompProperties_MechArmor : CompProperties
|
||||
{
|
||||
// 视觉效果
|
||||
public bool showBlockEffect = true;
|
||||
public ThingDef blockEffectMote; // 阻挡时显示的特效
|
||||
|
||||
// 音效
|
||||
public SoundDef soundOnBlock;
|
||||
|
||||
// 调试
|
||||
public bool debugLogging = false;
|
||||
|
||||
public CompProperties_MechArmor()
|
||||
{
|
||||
compClass = typeof(CompMechArmor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ namespace WulaFallenEmpire
|
||||
public ThingDef FuelType => Props.fuelType;
|
||||
|
||||
// 停机状态 Hediff
|
||||
private HediffDef ShutdownHediffDef => HediffDef.Named("DD_MechShutdown");
|
||||
private HediffDef ShutdownHediffDef => HediffDef.Named("WULA_MechShutdown");
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
@@ -162,7 +162,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
|
||||
// 播放关机效果
|
||||
MoteMaker.ThrowText(mech.DrawPos, mech.Map, "DD_Shutdown".Translate(), Color.gray, 3.5f);
|
||||
MoteMaker.ThrowText(mech.DrawPos, mech.Map, "WULA_Shutdown".Translate(), Color.gray, 3.5f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
}
|
||||
|
||||
MoteMaker.ThrowText(mech.DrawPos, mech.Map, "DD_Startup".Translate(), Color.green, 3.5f);
|
||||
MoteMaker.ThrowText(mech.DrawPos, mech.Map, "WULA_Startup".Translate(), Color.green, 3.5f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ namespace WulaFallenEmpire
|
||||
// 发送调试消息
|
||||
if (DebugSettings.godMode)
|
||||
{
|
||||
Messages.Message($"DD_Debug_FuelSet".Translate(
|
||||
Messages.Message($"WULA_Debug_FuelSet".Translate(
|
||||
parent.LabelShort,
|
||||
fuel.ToString("F1"),
|
||||
Props.fuelCapacity.ToString("F1"),
|
||||
@@ -267,7 +267,7 @@ namespace WulaFallenEmpire
|
||||
|
||||
if (bestColonist == null)
|
||||
{
|
||||
Messages.Message("DD_NoColonistAvailable".Translate(), parent, MessageTypeDefOf.RejectInput);
|
||||
Messages.Message("WULA_NoColonistAvailable".Translate(), parent, MessageTypeDefOf.RejectInput);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ namespace WulaFallenEmpire
|
||||
Thing fuel = FindFuelForRefuel(bestColonist);
|
||||
if (fuel == null)
|
||||
{
|
||||
Messages.Message("DD_NoFuelAvailable".Translate(FuelType), parent, MessageTypeDefOf.RejectInput);
|
||||
Messages.Message("WULA_NoFuelAvailable".Translate(FuelType), parent, MessageTypeDefOf.RejectInput);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ namespace WulaFallenEmpire
|
||||
bestColonist.jobs.StartJob(job, JobCondition.InterruptForced, null, resumeCurJobAfterwards: true);
|
||||
|
||||
// 显示消息
|
||||
Messages.Message("DD_OrderedRefuel".Translate(bestColonist.LabelShort, parent.LabelShort),
|
||||
Messages.Message("WULA_OrderedRefuel".Translate(bestColonist.LabelShort, parent.LabelShort),
|
||||
parent, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
|
||||
@@ -370,16 +370,16 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
Command_Action refuelNow = new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_RefuelNow".Translate(),
|
||||
defaultDesc = "DD_RefuelNowDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("WulaFallenEmpire/UI/Commands/DD_Refuel_Mech"),
|
||||
defaultLabel = "WULA_RefuelNow".Translate(),
|
||||
defaultDesc = "WULA_RefuelNowDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("WulaFallenEmpire/UI/Commands/WULA_Refuel_Mech"),
|
||||
action = () => RefuelNow()
|
||||
};
|
||||
|
||||
// 检查是否可以立刻加注
|
||||
if (!CanRefuelNow())
|
||||
{
|
||||
refuelNow.Disable("DD_CannotRefuelNow".Translate());
|
||||
refuelNow.Disable("WULA_CannotRefuelNow".Translate());
|
||||
}
|
||||
|
||||
yield return refuelNow;
|
||||
@@ -391,8 +391,8 @@ namespace WulaFallenEmpire
|
||||
// 设置燃料为空
|
||||
Command_Action setEmpty = new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_Debug_SetEmpty".Translate(),
|
||||
defaultDesc = "DD_Debug_SetEmptyDesc".Translate(),
|
||||
defaultLabel = "WULA_Debug_SetEmpty".Translate(),
|
||||
defaultDesc = "WULA_Debug_SetEmptyDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/SetEmpty", false) ?? BaseContent.BadTex,
|
||||
action = () => SetFuel(0f)
|
||||
};
|
||||
@@ -401,8 +401,8 @@ namespace WulaFallenEmpire
|
||||
// 设置燃料为50%
|
||||
Command_Action setHalf = new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_Debug_SetHalf".Translate(),
|
||||
defaultDesc = "DD_Debug_SetHalfDesc".Translate(),
|
||||
defaultLabel = "WULA_Debug_SetHalf".Translate(),
|
||||
defaultDesc = "WULA_Debug_SetHalfDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/SetHalf", false) ?? BaseContent.BadTex,
|
||||
action = () => SetFuel(Props.fuelCapacity * 0.5f)
|
||||
};
|
||||
@@ -411,8 +411,8 @@ namespace WulaFallenEmpire
|
||||
// 设置燃料为满
|
||||
Command_Action setFull = new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_Debug_SetFull".Translate(),
|
||||
defaultDesc = "DD_Debug_SetFullDesc".Translate(),
|
||||
defaultLabel = "WULA_Debug_SetFull".Translate(),
|
||||
defaultDesc = "WULA_Debug_SetFullDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/SetFull", false) ?? BaseContent.BadTex,
|
||||
action = () => SetFuel(Props.fuelCapacity)
|
||||
};
|
||||
@@ -433,7 +433,7 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
string baseString = base.CompInspectStringExtra();
|
||||
|
||||
string fuelString = "DD_Fuel".Translate(FuelType) + ": " +
|
||||
string fuelString = "WULA_Fuel".Translate(FuelType) + ": " +
|
||||
fuel.ToString("F1") + " / " + Props.fuelCapacity.ToString("F1") +
|
||||
" (" + (FuelPercent * 100f).ToString("F0") + "%)";
|
||||
|
||||
|
||||
@@ -49,8 +49,8 @@ namespace WulaFallenEmpire
|
||||
|
||||
// 在 God Mode 下显示"调试模式"标题
|
||||
string title = DebugSettings.godMode ?
|
||||
"DD_MechFuel".Translate().Resolve() + " [DEBUG]" :
|
||||
"DD_MechFuel".Translate().Resolve();
|
||||
"WULA_MechFuel".Translate().Resolve() + " [DEBUG]" :
|
||||
"WULA_MechFuel".Translate().Resolve();
|
||||
|
||||
Widgets.Label(titleRect, title);
|
||||
|
||||
@@ -88,13 +88,13 @@ namespace WulaFallenEmpire
|
||||
Text.Font = GameFont.Tiny;
|
||||
Text.Anchor = TextAnchor.UpperCenter;
|
||||
GUI.color = Color.red;
|
||||
Widgets.Label(statusRect, "DD_Shutdown".Translate());
|
||||
Widgets.Label(statusRect, "WULA_Shutdown".Translate());
|
||||
GUI.color = Color.white;
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
}
|
||||
|
||||
// 工具提示
|
||||
string tip = "DD_MechFuelTip".Translate(
|
||||
string tip = "WULA_MechFuelTip".Translate(
|
||||
fuelComp.FuelPercent.ToStringPercent(),
|
||||
fuelComp.Props.dailyFuelConsumption,
|
||||
fuelComp.Props.fuelType.label
|
||||
@@ -102,20 +102,20 @@ namespace WulaFallenEmpire
|
||||
|
||||
if (fuelComp.IsShutdown)
|
||||
{
|
||||
tip += "\n\n" + "DD_ShutdownTip".Translate();
|
||||
tip += "\n\n" + "WULA_ShutdownTip".Translate();
|
||||
}
|
||||
else if (fuelComp.NeedsRefueling)
|
||||
{
|
||||
tip += "\n\n" + "DD_NeedsRefueling".Translate();
|
||||
tip += "\n\n" + "WULA_NeedsRefueling".Translate();
|
||||
}
|
||||
|
||||
// 在 God Mode 下添加调试信息到工具提示
|
||||
if (DebugSettings.godMode)
|
||||
{
|
||||
tip += "\n\n" + "DD_Debug_Tip".Translate().Colorize(Color.gray) +
|
||||
"\n" + "DD_Debug_Status".Translate(
|
||||
fuelComp.IsShutdown ? "DD_Shutdown".Translate() : "DD_Running".Translate(),
|
||||
fuelComp.HasPilot() ? "DD_HasPilot".Translate() : "DD_NoPilot".Translate()
|
||||
tip += "\n\n" + "WULA_Debug_Tip".Translate().Colorize(Color.gray) +
|
||||
"\n" + "WULA_Debug_Status".Translate(
|
||||
fuelComp.IsShutdown ? "WULA_Shutdown".Translate() : "WULA_Running".Translate(),
|
||||
fuelComp.HasPilot() ? "WULA_HasPilot".Translate() : "WULA_NoPilot".Translate()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"[DD] Failed to create sustainer for {Props.movementSound.defName}");
|
||||
Log.Warning($"[WULA] Failed to create sustainer for {Props.movementSound.defName}");
|
||||
isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace WulaFallenEmpire
|
||||
// 如果需要驾驶员,检查是否配置了驾驶员容器
|
||||
if (requirePilot && parentDef.GetCompProperties<CompProperties_MechPilotHolder>() == null)
|
||||
{
|
||||
Log.Warning($"[DD] requirePilot is true but no CompProperties_MechPilotHolder found for {parentDef.defName}");
|
||||
Log.Warning($"[WULA] requirePilot is true but no CompProperties_MechPilotHolder found for {parentDef.defName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ namespace WulaFallenEmpire
|
||||
public string pilotWorkTag = "MechPilot";
|
||||
|
||||
// 新增:驾驶员图标配置
|
||||
public string summonPilotIcon = "WulaFallenEmpire/UI/Commands/DD_Enter_Mech";
|
||||
public string ejectPilotIcon = "WulaFallenEmpire/UI/Commands/DD_Exit_Mech";
|
||||
public string summonPilotIcon = "WulaFallenEmpire/UI/Commands/WULA_Enter_Mech";
|
||||
public string ejectPilotIcon = "WulaFallenEmpire/UI/Commands/WULA_Exit_Mech";
|
||||
|
||||
public float ejectPilotHealthPercentThreshold = 0.1f; // 默认30%血量
|
||||
public bool allowEntryBelowThreshold = false; // 血量低于阈值时是否允许进入
|
||||
@@ -204,7 +204,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[DD] 同步Hediff时出错: {ex}");
|
||||
Log.Error($"[WULA] 同步Hediff时出错: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[DD] 取消同步Hediff时出错: {ex}");
|
||||
Log.Error($"[WULA] 取消同步Hediff时出错: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[DD] 自动添加Hediff时出错: {ex}");
|
||||
Log.Error($"[WULA] 自动添加Hediff时出错: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[DD] CompTick error: {ex}");
|
||||
Log.Error($"[WULA] CompTick error: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[DD] 检查Hediff同步状态时出错: {ex}");
|
||||
Log.Error($"[WULA] 检查Hediff同步状态时出错: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ namespace WulaFallenEmpire
|
||||
|
||||
if (!(parent is Wulamechunit))
|
||||
{
|
||||
Log.Warning($"[DD] CompMechPilotHolder attached to non-mech: {parent}");
|
||||
Log.Warning($"[WULA] CompMechPilotHolder attached to non-mech: {parent}");
|
||||
}
|
||||
|
||||
// 确保加载后恢复状态
|
||||
@@ -567,7 +567,7 @@ namespace WulaFallenEmpire
|
||||
// 发送消息
|
||||
if (parent.Faction == Faction.OfPlayer)
|
||||
{
|
||||
Messages.Message("DD_PilotsEjectedDueToLowHealth".Translate(parent.LabelShort,
|
||||
Messages.Message("WULA_PilotsEjectedDueToLowHealth".Translate(parent.LabelShort,
|
||||
(Props.ejectPilotHealthPercentThreshold * 100).ToString("F0")),
|
||||
parent, MessageTypeDefOf.NegativeEvent);
|
||||
}
|
||||
@@ -605,8 +605,8 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
Command_Action summonCommand = new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_SummonPilot".Translate(),
|
||||
defaultDesc = "DD_SummonPilotDesc".Translate(),
|
||||
defaultLabel = "WULA_SummonPilot".Translate(),
|
||||
defaultDesc = "WULA_SummonPilotDesc".Translate(),
|
||||
icon = Props.GetSummonPilotIcon(),
|
||||
action = () =>
|
||||
{
|
||||
@@ -618,7 +618,7 @@ namespace WulaFallenEmpire
|
||||
// 如果血量低于阈值且不允许进入,禁用按钮
|
||||
if (!Props.allowEntryBelowThreshold && IsBelowHealthThreshold)
|
||||
{
|
||||
summonCommand.Disable("DD_MechTooDamagedForEntry".Translate());
|
||||
summonCommand.Disable("WULA_MechTooDamagedForEntry".Translate());
|
||||
}
|
||||
|
||||
yield return summonCommand;
|
||||
@@ -629,8 +629,8 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
yield return new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_EjectAllPilots".Translate(),
|
||||
defaultDesc = "DD_EjectAllPilotsDesc".Translate(),
|
||||
defaultLabel = "WULA_EjectAllPilots".Translate(),
|
||||
defaultDesc = "WULA_EjectAllPilotsDesc".Translate(),
|
||||
icon = Props.GetEjectPilotIcon(),
|
||||
action = () =>
|
||||
{
|
||||
@@ -723,14 +723,14 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[DD] 无法弹出驾驶员: {pawn.LabelShort}");
|
||||
Log.Error($"[WULA] 无法弹出驾驶员: {pawn.LabelShort}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[DD] 弹出驾驶员时发生错误: {ex}");
|
||||
Log.Error($"[WULA] 弹出驾驶员时发生错误: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -778,7 +778,7 @@ namespace WulaFallenEmpire
|
||||
Map map = parent.Map;
|
||||
if (map == null)
|
||||
{
|
||||
Log.Error($"[DD] 尝试在没有地图的情况下生成驾驶员: {pawn.LabelShort}");
|
||||
Log.Error($"[WULA] 尝试在没有地图的情况下生成驾驶员: {pawn.LabelShort}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[DD] 生成驾驶员时发生错误: {ex}");
|
||||
Log.Error($"[WULA] 生成驾驶员时发生错误: {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -841,7 +841,7 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
if (pilot.Faction == Faction.OfPlayer)
|
||||
{
|
||||
Messages.Message("DD_PilotEnteredMech".Translate(pilot.LabelShort, parent.LabelShort),
|
||||
Messages.Message("WULA_PilotEnteredMech".Translate(pilot.LabelShort, parent.LabelShort),
|
||||
parent, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
}
|
||||
@@ -850,7 +850,7 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
if (pilot.Faction == Faction.OfPlayer)
|
||||
{
|
||||
Messages.Message("DD_PilotExitedMech".Translate(pilot.LabelShort, parent.LabelShort),
|
||||
Messages.Message("WULA_PilotExitedMech".Translate(pilot.LabelShort, parent.LabelShort),
|
||||
parent, MessageTypeDefOf.NeutralEvent);
|
||||
}
|
||||
}
|
||||
@@ -899,7 +899,7 @@ namespace WulaFallenEmpire
|
||||
// 为能够行动的殖民者创建选项
|
||||
if (ableColonists.Count == 0 && disabledColonists.Count == 0)
|
||||
{
|
||||
options.Add(new FloatMenuOption("DD_NoAvailablePilots".Translate(), null));
|
||||
options.Add(new FloatMenuOption("WULA_NoAvailablePilots".Translate(), null));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -930,7 +930,7 @@ namespace WulaFallenEmpire
|
||||
// 无法行动的殖民者:需要搬运
|
||||
foreach (var colonist in disabledColonists)
|
||||
{
|
||||
string colonistLabel = colonist.LabelShortCap + " " + "DD_DisabledColonistRequiresCarry".Translate();
|
||||
string colonistLabel = colonist.LabelShortCap + " " + "WULA_DisabledColonistRequiresCarry".Translate();
|
||||
Action action = () => OrderCarryDisabledColonistToMech(colonist);
|
||||
|
||||
FloatMenuOption option = new FloatMenuOption(
|
||||
@@ -977,7 +977,7 @@ namespace WulaFallenEmpire
|
||||
|
||||
if (carrier == null)
|
||||
{
|
||||
Messages.Message("DD_NoAvailableCarrier".Translate(disabledColonist.LabelShortCap),
|
||||
Messages.Message("WULA_NoAvailableCarrier".Translate(disabledColonist.LabelShortCap),
|
||||
parent, MessageTypeDefOf.RejectInput);
|
||||
return;
|
||||
}
|
||||
@@ -986,7 +986,7 @@ namespace WulaFallenEmpire
|
||||
Job job = JobMaker.MakeJob(Wula_JobDefOf.WULA_CarryToMech, disabledColonist, mech);
|
||||
carrier.jobs.TryTakeOrderedJob(job, JobTag.Misc);
|
||||
|
||||
Messages.Message("DD_CarrierAssigned".Translate(carrier.LabelShortCap, disabledColonist.LabelShortCap),
|
||||
Messages.Message("WULA_CarrierAssigned".Translate(carrier.LabelShortCap, disabledColonist.LabelShortCap),
|
||||
parent, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,16 +84,16 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
Command_Action repairCommand = new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_ForceRepair".Translate(),
|
||||
defaultDesc = "DD_ForceRepairDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("WulaFallenEmpire/UI/Commands/DD_Repair_Mech"),
|
||||
defaultLabel = "WULA_ForceRepair".Translate(),
|
||||
defaultDesc = "WULA_ForceRepairDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("WulaFallenEmpire/UI/Commands/WULA_Repair_Mech"),
|
||||
action = () => ForceRepairNow()
|
||||
};
|
||||
|
||||
// 检查是否可以立即维修
|
||||
if (!CanRepairNow())
|
||||
{
|
||||
repairCommand.Disable("DD_CannotRepairNow".Translate());
|
||||
repairCommand.Disable("WULA_CannotRepairNow".Translate());
|
||||
}
|
||||
|
||||
yield return repairCommand;
|
||||
@@ -105,8 +105,8 @@ namespace WulaFallenEmpire
|
||||
// 模拟受伤按钮
|
||||
Command_Action damageCommand = new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_Debug_Damage".Translate(),
|
||||
defaultDesc = "DD_Debug_DamageDesc".Translate(),
|
||||
defaultLabel = "WULA_Debug_Damage".Translate(),
|
||||
defaultDesc = "WULA_Debug_DamageDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/Damage", false) ?? BaseContent.BadTex,
|
||||
action = () => DebugDamage()
|
||||
};
|
||||
@@ -115,8 +115,8 @@ namespace WulaFallenEmpire
|
||||
// 完全修复按钮
|
||||
Command_Action fullRepairCommand = new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_Debug_FullRepair".Translate(),
|
||||
defaultDesc = "DD_Debug_FullRepairDesc".Translate(),
|
||||
defaultLabel = "WULA_Debug_FullRepair".Translate(),
|
||||
defaultDesc = "WULA_Debug_FullRepairDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/Repair", false) ?? BaseContent.BadTex,
|
||||
action = () => DebugFullRepair()
|
||||
};
|
||||
@@ -125,8 +125,8 @@ namespace WulaFallenEmpire
|
||||
// 显示维修统计
|
||||
Command_Action statsCommand = new Command_Action
|
||||
{
|
||||
defaultLabel = "DD_Debug_RepairStats".Translate(),
|
||||
defaultDesc = "DD_Debug_RepairStatsDesc".Translate(totalRepairedHP.ToString("F1")),
|
||||
defaultLabel = "WULA_Debug_RepairStats".Translate(),
|
||||
defaultDesc = "WULA_Debug_RepairStatsDesc".Translate(totalRepairedHP.ToString("F1")),
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/Stats", false) ?? BaseContent.BadTex,
|
||||
action = () => DebugShowStats()
|
||||
};
|
||||
@@ -145,7 +145,7 @@ namespace WulaFallenEmpire
|
||||
|
||||
if (bestColonist == null)
|
||||
{
|
||||
Messages.Message("DD_NoColonistAvailable".Translate(), parent, MessageTypeDefOf.RejectInput);
|
||||
Messages.Message("WULA_NoColonistAvailable".Translate(), parent, MessageTypeDefOf.RejectInput);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace WulaFallenEmpire
|
||||
bestColonist.jobs.StartJob(job, JobCondition.InterruptForced, null, resumeCurJobAfterwards: true);
|
||||
|
||||
// 显示消息
|
||||
Messages.Message("DD_OrderedRepair".Translate(bestColonist.LabelShort, parent.LabelShort),
|
||||
Messages.Message("WULA_OrderedRepair".Translate(bestColonist.LabelShort, parent.LabelShort),
|
||||
parent, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ namespace WulaFallenEmpire
|
||||
DamageInfo dinfo = new DamageInfo(DamageDefOf.Cut, damage, 1f, -1f, null, part);
|
||||
mech.TakeDamage(dinfo);
|
||||
|
||||
Messages.Message($"DD_Debug_Damaged".Translate(parent.LabelShort, damage.ToString("F1")),
|
||||
Messages.Message($"WULA_Debug_Damaged".Translate(parent.LabelShort, damage.ToString("F1")),
|
||||
parent, MessageTypeDefOf.NeutralEvent);
|
||||
}
|
||||
|
||||
@@ -246,14 +246,14 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
}
|
||||
|
||||
Messages.Message($"DD_Debug_FullyRepaired".Translate(parent.LabelShort),
|
||||
Messages.Message($"WULA_Debug_FullyRepaired".Translate(parent.LabelShort),
|
||||
parent, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
|
||||
// 调试功能:显示维修统计
|
||||
private void DebugShowStats()
|
||||
{
|
||||
Messages.Message($"DD_Debug_RepairStatsInfo".Translate(
|
||||
Messages.Message($"WULA_Debug_RepairStatsInfo".Translate(
|
||||
parent.LabelShort,
|
||||
totalRepairedHP.ToString("F1"),
|
||||
Props.repairAmountPerCycle.ToString("F1"),
|
||||
@@ -277,7 +277,7 @@ namespace WulaFallenEmpire
|
||||
string repairString = "";
|
||||
if (NeedsRepair)
|
||||
{
|
||||
repairString = "DD_NeedsRepair".Translate().Colorize(Color.yellow);
|
||||
repairString = "WULA_NeedsRepair".Translate().Colorize(Color.yellow);
|
||||
}
|
||||
|
||||
if (!baseString.NullOrEmpty())
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace WulaFallenEmpire
|
||||
// 如果需要驾驶员但组件不存在,发出警告
|
||||
if (Props.requirePilot && pilotHolder == null)
|
||||
{
|
||||
Log.Warning($"[DD] CompMoteEmitterNorthward on {parent} requires pilot but no CompMechPilotHolder found");
|
||||
Log.Warning($"[WULA] CompMoteEmitterNorthward on {parent} requires pilot but no CompMechPilotHolder found");
|
||||
}
|
||||
|
||||
// 初始化位置
|
||||
@@ -154,7 +154,7 @@ namespace WulaFallenEmpire
|
||||
catch (NullReferenceException ex)
|
||||
{
|
||||
// 发生异常时重置状态
|
||||
Log.Warning($"[DD] Error updating movement state for {parent}: {ex.Message}");
|
||||
Log.Warning($"[WULA] Error updating movement state for {parent}: {ex.Message}");
|
||||
isMoving = false;
|
||||
}
|
||||
}
|
||||
@@ -284,7 +284,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[DD] Error emitting mote: {ex}");
|
||||
Log.Error($"[WULA] Error emitting mote: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Reflection.Emit;
|
||||
using System.Runtime.Remoting.Messaging;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_PawnRenderExtra : CompProperties
|
||||
{
|
||||
public CompProperties_PawnRenderExtra()
|
||||
{
|
||||
this.compClass = typeof(Comp_PawnRenderExtra);
|
||||
}
|
||||
|
||||
public string path;
|
||||
|
||||
public Vector3 size;
|
||||
|
||||
public Color colorAlly;
|
||||
|
||||
public Color colorEnemy;
|
||||
|
||||
public ShaderTypeDef shader;
|
||||
|
||||
public DrawData drawData;
|
||||
}
|
||||
[StaticConstructorOnStartup]
|
||||
public class Comp_PawnRenderExtra : ThingComp
|
||||
{
|
||||
public CompProperties_PawnRenderExtra Props
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.props as CompProperties_PawnRenderExtra;
|
||||
}
|
||||
}
|
||||
|
||||
private Pawn ParentPawn
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.parent as Pawn;
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostDraw()
|
||||
{
|
||||
base.PostDraw();
|
||||
if (!this.ParentPawn.Dead && !this.ParentPawn.Downed && this.ParentPawn.CurJobDef != JobDefOf.MechCharge && this.ParentPawn.CurJobDef != JobDefOf.SelfShutdown)
|
||||
{
|
||||
this.DrawPawnRenderExtra();
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawPawnRenderExtra()
|
||||
{
|
||||
Vector3 pos = this.ParentPawn.DrawPos;
|
||||
if (this.ParentPawn.Faction == Faction.OfPlayer || !this.ParentPawn.Faction.HostileTo(Faction.OfPlayer))
|
||||
{
|
||||
this.color = this.Props.colorAlly;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.color = this.Props.colorEnemy;
|
||||
}
|
||||
string graphic = this.GetPawnRenderExtra();
|
||||
Vector3 offset = GetOffsetByRot();
|
||||
float layer = GetLayerByRot();
|
||||
pos.y = AltitudeLayer.Pawn.AltitudeFor(layer);
|
||||
|
||||
Matrix4x4 matrix = default(Matrix4x4);
|
||||
matrix.SetTRS(pos + offset, Quaternion.AngleAxis(0f, Vector3.up), this.Props.size);
|
||||
Material material = MaterialPool.MatFrom(graphic, this.Props.shader.Shader, this.color);
|
||||
Graphics.DrawMesh(MeshPool.plane10, matrix, material, (int)layer);
|
||||
}
|
||||
|
||||
public Vector3 GetOffsetByRot()
|
||||
{
|
||||
Vector3 result;
|
||||
if (this.Props.drawData != null)
|
||||
{
|
||||
result = this.Props.drawData.OffsetForRot(this.ParentPawn.Rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Vector3.zero;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public float GetLayerByRot()
|
||||
{
|
||||
float result;
|
||||
if (this.Props.drawData != null)
|
||||
{
|
||||
result = this.Props.drawData.LayerForRot(this.ParentPawn.Rotation, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GetPawnRenderExtra()
|
||||
{
|
||||
if (this.ParentPawn.Rotation.AsInt == 0)
|
||||
{
|
||||
return this.Props.path + "_north";
|
||||
}
|
||||
if (this.ParentPawn.Rotation.AsInt == 1)
|
||||
{
|
||||
return this.Props.path + "_east";
|
||||
}
|
||||
if (this.ParentPawn.Rotation.AsInt == 2)
|
||||
{
|
||||
return this.Props.path + "_south";
|
||||
}
|
||||
if (this.ParentPawn.Rotation.AsInt == 3)
|
||||
{
|
||||
return this.Props.path + "_west";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public Color color;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// A marker component that holds custom flight properties.
|
||||
/// The actual flight logic is handled by Harmony patches that check for this component
|
||||
/// and use its properties to override or trigger vanilla flight behavior.
|
||||
/// </summary>
|
||||
public class CompPawnFlight : ThingComp
|
||||
{
|
||||
public CompProperties_PawnFlight Props => (CompProperties_PawnFlight)props;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Verse;
|
||||
using RimWorld;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public enum FlightCondition
|
||||
{
|
||||
Drafted,
|
||||
DraftedAndMove,
|
||||
Always
|
||||
}
|
||||
|
||||
public class CompProperties_PawnFlight : CompProperties
|
||||
{
|
||||
// --- Custom Flight Logic ---
|
||||
public FlightCondition flightCondition = FlightCondition.Drafted;
|
||||
|
||||
// --- Vanilla PawnKindDef Flight Parameters ---
|
||||
[NoTranslate]
|
||||
public string flyingAnimationFramePathPrefix;
|
||||
|
||||
[NoTranslate]
|
||||
public string flyingAnimationFramePathPrefixFemale;
|
||||
|
||||
public int flyingAnimationFrameCount;
|
||||
|
||||
public int flyingAnimationTicksPerFrame = -1;
|
||||
|
||||
public float flyingAnimationDrawSize = 1f;
|
||||
|
||||
public bool flyingAnimationDrawSizeIsMultiplier;
|
||||
|
||||
public bool flyingAnimationInheritColors;
|
||||
|
||||
// --- Vanilla PawnKindLifeStage Flight Parameters ---
|
||||
// Note: These are normally defined per lifestage, we define them once here for simplicity.
|
||||
// The harmony patch will need to inject these into the correct lifestage at runtime.
|
||||
public AnimationDef flyingAnimationEast;
|
||||
public AnimationDef flyingAnimationNorth;
|
||||
public AnimationDef flyingAnimationSouth;
|
||||
public AnimationDef flyingAnimationEastFemale;
|
||||
public AnimationDef flyingAnimationNorthFemale;
|
||||
public AnimationDef flyingAnimationSouthFemale;
|
||||
|
||||
|
||||
public CompProperties_PawnFlight()
|
||||
{
|
||||
compClass = typeof(CompPawnFlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Verse;
|
||||
using UnityEngine;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class PawnRenderNodeWorker_AttachmentBody_NoFlight : PawnRenderNodeWorker_AttachmentBody
|
||||
{
|
||||
public override bool CanDrawNow(PawnRenderNode node, PawnDrawParms parms)
|
||||
{
|
||||
if (parms.pawn.Flying)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return base.CanDrawNow(node, parms);
|
||||
}
|
||||
|
||||
public override Vector3 ScaleFor(PawnRenderNode node, PawnDrawParms parms)
|
||||
{
|
||||
return base.ScaleFor(node, parms);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using HarmonyLib;
|
||||
using Verse;
|
||||
using RimWorld;
|
||||
using Verse.AI;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
[HarmonyPatch]
|
||||
public static class FlightHarmonyPatches
|
||||
{
|
||||
// Corrected Patch 1: The method signature now correctly matches the static target method.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Pawn_FlightTracker), "GetBestFlyAnimation")]
|
||||
public static bool GetBestFlyAnimation_Prefix(Pawn pawn, ref AnimationDef __result) // Correct parameters: Pawn pawn, not __instance and ___pawn
|
||||
{
|
||||
var flightComp = pawn?.TryGetComp<CompPawnFlight>();
|
||||
if (flightComp == null) // No props check needed, as the crash was due to wrong signature
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var compProps = flightComp.Props;
|
||||
AnimationDef selectedAnim = null;
|
||||
|
||||
if (pawn.gender == Gender.Female && compProps.flyingAnimationNorthFemale != null)
|
||||
{
|
||||
switch (pawn.Rotation.AsInt)
|
||||
{
|
||||
case 0: selectedAnim = compProps.flyingAnimationNorthFemale; break;
|
||||
case 1: selectedAnim = compProps.flyingAnimationEastFemale; break;
|
||||
case 2: selectedAnim = compProps.flyingAnimationSouthFemale; break;
|
||||
case 3: selectedAnim = compProps.flyingAnimationEastFemale ?? compProps.flyingAnimationEast; break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (pawn.Rotation.AsInt)
|
||||
{
|
||||
case 0: selectedAnim = compProps.flyingAnimationNorth; break;
|
||||
case 1: selectedAnim = compProps.flyingAnimationEast; break;
|
||||
case 2: selectedAnim = compProps.flyingAnimationSouth; break;
|
||||
case 3: selectedAnim = compProps.flyingAnimationEast; break;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedAnim != null)
|
||||
{
|
||||
__result = selectedAnim;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Patch 2 remains correct as Notify_JobStarted is a non-static method.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Pawn_FlightTracker), "Notify_JobStarted")]
|
||||
public static bool Notify_JobStarted_Prefix(Job job, Pawn_FlightTracker __instance, Pawn ___pawn)
|
||||
{
|
||||
var flightComp = ___pawn?.TryGetComp<CompPawnFlight>();
|
||||
if (flightComp == null || __instance == null || !__instance.CanEverFly || ___pawn == null || ___pawn.Dead)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool shouldBeFlying = false;
|
||||
var compProps = flightComp.Props;
|
||||
if (compProps.flightCondition == FlightCondition.Always)
|
||||
{
|
||||
shouldBeFlying = true;
|
||||
}
|
||||
else if (compProps.flightCondition == FlightCondition.DraftedAndMove && ___pawn.Drafted || ___pawn.pather.MovingNow)
|
||||
{
|
||||
shouldBeFlying = true;
|
||||
}
|
||||
else if (compProps.flightCondition == FlightCondition.Drafted && ___pawn.Drafted)
|
||||
{
|
||||
shouldBeFlying = true;
|
||||
}
|
||||
|
||||
if (shouldBeFlying)
|
||||
{
|
||||
if (!__instance.Flying) __instance.StartFlying();
|
||||
job.flying = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (__instance.Flying) __instance.ForceLand();
|
||||
job.flying = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user