This commit is contained in:
2026-02-15 16:32:55 +08:00
parent c7a520b2f3
commit 2eabf020fd
26 changed files with 1743 additions and 281 deletions

View File

@@ -2,6 +2,18 @@
"Version": 1,
"WorkspaceRootPath": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|d:\\steamlibrary\\steamapps\\common\\rimworld\\mods\\arachnaeswarm\\source\\arachnaeswarm\\abilities\\ara_fanshapedstunknockback\\compproperties_abilityfanshapedstunknockback.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|solutionrelative:abilities\\ara_fanshapedstunknockback\\compproperties_abilityfanshapedstunknockback.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|d:\\steamlibrary\\steamapps\\common\\rimworld\\mods\\arachnaeswarm\\source\\arachnaeswarm\\abilities\\ara_fanshapedstunknockback\\compabilityeffect_fanshapedstunknockback.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|solutionrelative:abilities\\ara_fanshapedstunknockback\\compabilityeffect_fanshapedstunknockback.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|d:\\steamlibrary\\steamapps\\common\\rimworld\\mods\\arachnaeswarm\\source\\arachnaeswarm\\abilities\\ara_ejectorgans\\compabilityeffect_ejectorgans.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|solutionrelative:abilities\\ara_ejectorgans\\compabilityeffect_ejectorgans.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|d:\\steamlibrary\\steamapps\\common\\rimworld\\mods\\arachnaeswarm\\source\\arachnaeswarm\\pawn_comps\\ara_comphediffgiver\\compproperties_hediffgiver.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|solutionrelative:pawn_comps\\ara_comphediffgiver\\compproperties_hediffgiver.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
@@ -18,15 +30,54 @@
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 2,
"SelectedChildIndex": 1,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "CompProperties_AbilityFanShapedStunKnockback.cs",
"DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Abilities\\ARA_FanShapedStunKnockback\\CompProperties_AbilityFanShapedStunKnockback.cs",
"RelativeDocumentMoniker": "Abilities\\ARA_FanShapedStunKnockback\\CompProperties_AbilityFanShapedStunKnockback.cs",
"ToolTip": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Abilities\\ARA_FanShapedStunKnockback\\CompProperties_AbilityFanShapedStunKnockback.cs",
"RelativeToolTip": "Abilities\\ARA_FanShapedStunKnockback\\CompProperties_AbilityFanShapedStunKnockback.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAUAAAA9AAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-15T06:29:46.581Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "CompAbilityEffect_FanShapedStunKnockback.cs",
"DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Abilities\\ARA_FanShapedStunKnockback\\CompAbilityEffect_FanShapedStunKnockback.cs",
"RelativeDocumentMoniker": "Abilities\\ARA_FanShapedStunKnockback\\CompAbilityEffect_FanShapedStunKnockback.cs",
"ToolTip": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Abilities\\ARA_FanShapedStunKnockback\\CompAbilityEffect_FanShapedStunKnockback.cs",
"RelativeToolTip": "Abilities\\ARA_FanShapedStunKnockback\\CompAbilityEffect_FanShapedStunKnockback.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-15T06:29:34.172Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 2,
"Title": "CompAbilityEffect_EjectOrgans.cs",
"DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Abilities\\ARA_EjectOrgans\\CompAbilityEffect_EjectOrgans.cs",
"RelativeDocumentMoniker": "Abilities\\ARA_EjectOrgans\\CompAbilityEffect_EjectOrgans.cs",
"ToolTip": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Abilities\\ARA_EjectOrgans\\CompAbilityEffect_EjectOrgans.cs",
"RelativeToolTip": "Abilities\\ARA_EjectOrgans\\CompAbilityEffect_EjectOrgans.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAXAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-15T06:29:29.494Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 4,
"Title": "CompHediffGiver.cs",
"DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Pawn_Comps\\ARA_CompHediffGiver\\CompHediffGiver.cs",
"RelativeDocumentMoniker": "Pawn_Comps\\ARA_CompHediffGiver\\CompHediffGiver.cs",
@@ -38,7 +89,7 @@
},
{
"$type": "Document",
"DocumentIndex": 0,
"DocumentIndex": 3,
"Title": "CompProperties_HediffGiver.cs",
"DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Pawn_Comps\\ARA_CompHediffGiver\\CompProperties_HediffGiver.cs",
"RelativeDocumentMoniker": "Pawn_Comps\\ARA_CompHediffGiver\\CompProperties_HediffGiver.cs",

View File

@@ -0,0 +1,722 @@
using RimWorld;
using System.Collections.Generic;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace ArachnaeSwarm
{
public class CompAbilityEffect_FanShapedStunKnockback : CompAbilityEffect
{
private readonly List<IntVec3> tmpCells = new List<IntVec3>();
private Effecter effecter;
public new CompProperties_AbilityFanShapedStunKnockback Props => (CompProperties_AbilityFanShapedStunKnockback)props;
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
{
base.Apply(target, dest);
Pawn caster = parent.pawn;
if (caster == null || caster.Map == null || !target.IsValid)
return;
// 1. 获取扇形区域内的所有单元格
List<IntVec3> affectedCells = GetFanShapedCells(caster, target.Cell);
// 2. 收集区域内的所有目标
var affectedTargets = CollectAffectedTargets(caster, affectedCells);
// 3. 对每个目标应用效果
foreach (Thing targetThing in affectedTargets)
{
if (targetThing != null && !targetThing.Destroyed && targetThing.Spawned)
{
if (targetThing is Pawn pawn)
{
ApplyEffectToPawn(caster, pawn, target);
}
else if (Props.affectNonPawnThings)
{
ApplyEffectToNonPawnThing(caster, targetThing, target);
}
}
}
// 4. 播放整体攻击效果参考Verb_MeleeAttack_Cleave
PlayMainAttackEffect(caster, target);
}
/// <summary>
/// 播放主要攻击效果
/// </summary>
private void PlayMainAttackEffect(Pawn caster, LocalTargetInfo target)
{
if (caster == null || caster.Map == null || !target.IsValid)
return;
// 播放主要攻击效果
if (Props.impactEffecter != null)
{
effecter = Props.impactEffecter.Spawn();
// 关键修复:第一个参数是施法者位置,第二个参数是目标位置
effecter.Trigger(new TargetInfo(caster.Position, caster.Map), target.ToTargetInfo(caster.Map));
effecter.Cleanup();
effecter = null;
}
// 播放攻击音效
if (Props.impactSound != null)
{
Props.impactSound.PlayOneShot(new TargetInfo(target.Cell, caster.Map));
}
}
/// <summary>
/// 收集扇形区域内的所有目标包括Pawn和非Pawn物体
/// </summary>
private List<Thing> CollectAffectedTargets(Pawn caster, List<IntVec3> cells)
{
List<Thing> targets = new List<Thing>();
HashSet<Thing> addedThings = new HashSet<Thing>();
if (caster == null || caster.Map == null || cells == null)
return targets;
foreach (IntVec3 cell in cells)
{
if (!cell.InBounds(caster.Map))
continue;
List<Thing> things = cell.GetThingList(caster.Map);
foreach (Thing thing in things)
{
if (thing == null || addedThings.Contains(thing))
continue;
// 检查是否为施法者
if (!Props.affectCaster && thing == caster)
continue;
// 检查是否需要视线
if (Props.requireLineOfSightToTarget && !GenSight.LineOfSight(caster.Position, cell, caster.Map))
continue;
// Pawn的处理
if (thing is Pawn pawn)
{
// 检查是否为敌人
if (Props.onlyAffectEnemies && !pawn.HostileTo(caster))
continue;
targets.Add(pawn);
addedThings.Add(pawn);
}
// 非Pawn物体的处理
else if (Props.affectNonPawnThings && thing is ThingWithComps thingWithComps)
{
// 检查是否为敌人(如果物体有派系)
if (Props.onlyAffectEnemies)
{
bool isEnemy = IsThingEnemy(caster, thingWithComps);
if (!isEnemy)
continue;
}
// 检查是否可以被伤害
if (Props.canDamageNonPawnThings && !CanBeDamaged(thingWithComps))
continue;
targets.Add(thingWithComps);
addedThings.Add(thingWithComps);
}
}
}
return targets;
}
/// <summary>
/// 检查物体是否为敌人
/// </summary>
private bool IsThingEnemy(Pawn caster, Thing thing)
{
// 如果物体有派系,检查是否敌对
if (thing.Faction != null)
{
return caster.HostileTo(thing);
}
// 如果物体是建筑且没有派系,根据设置决定
// 默认情况下视为中立只有当onlyAffectEnemies为false时才影响
return false;
}
/// <summary>
/// 检查物体是否可以被伤害
/// </summary>
private bool CanBeDamaged(Thing thing)
{
// 检查是否有生命值组件
if (thing.def.useHitPoints)
{
// 检查是否被摧毁或已死亡
if (thing.Destroyed || thing.HitPoints <= 0)
return false;
// 检查是否可以承受伤害
if (thing.def.destroyable)
return true;
}
return false;
}
/// <summary>
/// 对非Pawn物体应用效果
/// </summary>
private void ApplyEffectToNonPawnThing(Pawn caster, Thing targetThing, LocalTargetInfo targetInfo)
{
if (targetThing == null || caster == null)
return;
// 1. 造成伤害
ApplyDamageToNonPawn(caster, targetThing);
// 注意非Pawn物体不进行击退也不眩晕
}
/// <summary>
/// 对非Pawn物体造成伤害
/// </summary>
private void ApplyDamageToNonPawn(Pawn caster, Thing targetThing)
{
if (!Props.canDamageNonPawnThings)
return;
// 获取调整后的伤害值
float adjustedDamage = GetAdjustedDamage(caster);
// 应用非Pawn物体的伤害倍率
float finalDamage = adjustedDamage * Props.nonPawnDamageMultiplier;
// 检查目标是否可以被伤害
if (targetThing.def.useHitPoints && targetThing.HitPoints > 0)
{
// 创建伤害信息
DamageInfo damageInfo = new DamageInfo(
Props.damageDef,
finalDamage,
Props.armorPenetration,
-1f,
caster,
null
);
// 应用伤害
targetThing.TakeDamage(damageInfo);
// 播放个体命中效果
if (Props.applySpecialEffectsToNonPawn && Props.impactEffecter != null && caster.Map != null)
{
Effecter effect = Props.impactEffecter.Spawn();
// 关键修复:第一个参数是施法者,第二个参数是目标
effect.Trigger(new TargetInfo(caster.Position, caster.Map), new TargetInfo(targetThing.Position, caster.Map));
effect.Cleanup();
}
// 播放个体命中音效
if (Props.applySpecialEffectsToNonPawn && Props.impactSound != null && caster.Map != null)
{
Props.impactSound.PlayOneShot(new TargetInfo(targetThing.Position, caster.Map));
}
}
}
/// <summary>
/// 获取伤害系数
/// </summary>
private float GetDamageMultiplier(Pawn caster)
{
if (caster == null) return 1f;
if (Props.multiplyDamageByMeleeFactor)
{
if (Props.damageMultiplierStat != null)
{
return caster.GetStatValue(Props.damageMultiplierStat);
}
return caster.GetStatValue(StatDefOf.MeleeDamageFactor);
}
return 1f;
}
/// <summary>
/// 获取眩晕时间系数
/// </summary>
private float GetStunMultiplier(Pawn caster)
{
if (caster == null) return 1f;
if (Props.multiplyStunTimeByMeleeFactor)
{
if (Props.stunMultiplierStat != null)
{
return caster.GetStatValue(Props.stunMultiplierStat);
}
return caster.GetStatValue(StatDefOf.MeleeDamageFactor);
}
return 1f;
}
/// <summary>
/// 获取调整后的伤害值
/// </summary>
private float GetAdjustedDamage(Pawn caster)
{
float baseDamage = Props.damageAmount;
float multiplier = GetDamageMultiplier(caster);
return baseDamage * multiplier;
}
/// <summary>
/// 获取调整后的眩晕时间
/// </summary>
private int GetAdjustedStunTicks(Pawn caster)
{
int baseStunTicks = Props.stunTicks;
float multiplier = GetStunMultiplier(caster);
return Mathf.RoundToInt(baseStunTicks * multiplier);
}
/// <summary>
/// 显示伤害和眩晕加成信息(用于预览)
/// </summary>
public string GetAdjustedDamageAndStunInfo(Pawn caster)
{
if (caster == null) return string.Empty;
float damageMultiplier = GetDamageMultiplier(caster);
float stunMultiplier = GetStunMultiplier(caster);
// 如果都不需要乘以系数,则不显示信息
if (damageMultiplier == 1f && stunMultiplier == 1f)
{
return string.Empty;
}
var sb = new System.Text.StringBuilder();
sb.AppendLine("LBD_AdjustedEffects".Translate());
if (damageMultiplier != 1f)
{
float adjustedDamage = Props.damageAmount * damageMultiplier;
sb.AppendLine("LBD_AdjustedDamage".Translate(Props.damageAmount, adjustedDamage));
}
if (stunMultiplier != 1f)
{
int adjustedStunTicks = Mathf.RoundToInt(Props.stunTicks * stunMultiplier);
float baseStunSeconds = Props.stunTicks / 60f;
float adjustedStunSeconds = adjustedStunTicks / 60f;
sb.AppendLine("LBD_AdjustedStun".Translate(baseStunSeconds, adjustedStunSeconds));
}
return sb.ToString();
}
/// <summary>
/// 获取扇形区域内的所有单元格
/// </summary>
private List<IntVec3> GetFanShapedCells(Pawn caster, IntVec3 targetCell)
{
tmpCells.Clear();
if (caster == null || caster.Map == null)
return tmpCells;
IntVec3 casterPos = caster.Position;
IntVec3 clampedTarget = targetCell.ClampInsideMap(caster.Map);
// 如果施法者和目标在同一位置,则没有扇形
if (casterPos == clampedTarget)
return tmpCells;
Vector3 casterVector = casterPos.ToVector3Shifted().Yto0();
// 计算方向向量和角度
float horizontalLength = (clampedTarget - casterPos).LengthHorizontal;
float dirX = (clampedTarget.x - casterPos.x) / horizontalLength;
float dirZ = (clampedTarget.z - casterPos.z) / horizontalLength;
// 调整目标点到扇形半径
clampedTarget.x = Mathf.RoundToInt(casterPos.x + dirX * Props.range);
clampedTarget.z = Mathf.RoundToInt(casterPos.z + dirZ * Props.range);
// 计算扇形的中心角
float targetAngle = Vector3.SignedAngle(
clampedTarget.ToVector3Shifted().Yto0() - casterVector,
Vector3.right,
Vector3.up);
// 计算扇形的半角(从中心线到边缘)
float halfWidth = Props.lineWidthEnd / 2f;
float coneEdgeDistance = Mathf.Sqrt(
Mathf.Pow((clampedTarget - casterPos).LengthHorizontal, 2f) +
Mathf.Pow(halfWidth, 2f));
float halfAngle = Mathf.Rad2Deg * Mathf.Asin(halfWidth / coneEdgeDistance);
// 限制最大角度不超过设定值
halfAngle = Mathf.Min(halfAngle, Props.coneSizeDegrees / 2f);
// 遍历半径内的所有单元格,检查是否在扇形内
int radialCellCount = GenRadial.NumCellsInRadius(Props.range);
for (int i = 0; i < radialCellCount; i++)
{
IntVec3 cell = casterPos + GenRadial.RadialPattern[i];
// 检查单元格是否有效
if (!CanUseCell(caster, cell))
continue;
// 计算单元格相对于施法者的角度
float cellAngle = Vector3.SignedAngle(
cell.ToVector3Shifted().Yto0() - casterVector,
Vector3.right,
Vector3.up);
// 检查角度差是否在扇形范围内
if (Mathf.Abs(Mathf.DeltaAngle(cellAngle, targetAngle)) <= halfAngle)
{
tmpCells.Add(cell);
}
}
// 添加从施法者到目标点的直线上的单元格
List<IntVec3> lineCells = GenSight.BresenhamCellsBetween(casterPos, clampedTarget);
for (int i = 0; i < lineCells.Count; i++)
{
IntVec3 cell = lineCells[i];
if (!tmpCells.Contains(cell) && CanUseCell(caster, cell))
{
tmpCells.Add(cell);
}
}
return tmpCells;
}
/// <summary>
/// 对单个Pawn应用效果
/// </summary>
private void ApplyEffectToPawn(Pawn caster, Pawn target, LocalTargetInfo targetInfo)
{
// 1. 造成伤害
bool targetDied = ApplyDamageAndStun(caster, target);
// 2. 如果目标存活,执行击退
if (!targetDied && target != null && !target.Dead && !target.Downed)
{
PerformKnockback(caster, target);
}
}
/// <summary>
/// 应用伤害和眩晕效果,返回目标是否死亡
/// </summary>
private bool ApplyDamageAndStun(Pawn caster, Pawn target)
{
// 获取调整后的伤害和眩晕时间
float adjustedDamage = GetAdjustedDamage(caster);
int adjustedStunTicks = GetAdjustedStunTicks(caster);
// 创建伤害信息
DamageInfo damageInfo = new DamageInfo(
Props.damageDef,
adjustedDamage,
Props.armorPenetration,
-1f,
caster,
null
);
// 应用伤害
target.TakeDamage(damageInfo);
// 检查目标是否死亡
bool targetDied = target.Dead || target.Destroyed;
if (targetDied)
{
return true;
}
// 播放个体命中效果(可选,因为已经有主要攻击效果)
if (Props.applySpecialEffectsToNonPawn && Props.impactEffecter != null && caster.Map != null)
{
Effecter effect = Props.impactEffecter.Spawn();
// 关键修复:第一个参数是施法者,第二个参数是目标
effect.Trigger(new TargetInfo(caster.Position, caster.Map), new TargetInfo(target.Position, caster.Map));
effect.Cleanup();
}
// 应用眩晕 - 只在目标存活时应用
if (adjustedStunTicks > 0 && !target.Dead)
{
target.stances?.stunner?.StunFor(adjustedStunTicks, caster);
}
return false;
}
/// <summary>
/// 执行击退
/// </summary>
private void PerformKnockback(Pawn caster, Pawn target)
{
if (target == null || target.Destroyed || target.Dead || caster.Map == null)
return;
// 计算击退方向(从施法者指向目标)
IntVec3 knockbackDirection = CalculateKnockbackDirection(caster, target.Position);
// 寻找最远的可站立击退位置
IntVec3 knockbackDestination = FindFarthestStandablePosition(caster, target, knockbackDirection);
// 如果找到了有效位置,执行击退飞行
if (knockbackDestination.IsValid && knockbackDestination != target.Position)
{
CreateKnockbackFlyer(caster, target, knockbackDestination);
}
}
/// <summary>
/// 计算击退方向
/// </summary>
private IntVec3 CalculateKnockbackDirection(Pawn caster, IntVec3 targetPosition)
{
IntVec3 direction = targetPosition - caster.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 FindFarthestStandablePosition(Pawn caster, Pawn target, IntVec3 direction)
{
Map map = caster.Map;
IntVec3 currentPos = target.Position;
IntVec3 farthestValidPos = currentPos;
// 从最大距离开始向回找,找到第一个可站立的格子
for (int distance = Props.maxKnockbackDistance; distance >= 1; distance--)
{
IntVec3 testPos = currentPos + (direction * distance);
if (!testPos.InBounds(map))
continue;
if (IsCellStandableAndEmpty(caster, target, testPos, map))
{
farthestValidPos = testPos;
break;
}
}
return farthestValidPos;
}
/// <summary>
/// 检查格子是否可站立且没有其他Pawn
/// </summary>
private bool IsCellStandableAndEmpty(Pawn caster, Pawn target, IntVec3 cell, Map map)
{
if (!cell.InBounds(map))
return false;
// 检查是否可站立
if (!cell.Standable(map))
return false;
// 检查是否有建筑阻挡
if (!Props.canKnockbackIntoWalls)
{
Building edifice = cell.GetEdifice(map);
if (edifice != null && !(edifice is Building_Door))
return false;
}
// 检查视线
if (Props.requireLineOfSight && !GenSight.LineOfSight(target.Position, cell, map))
return false;
// 检查是否有其他pawn
List<Thing> thingList = cell.GetThingList(map);
foreach (Thing thing in thingList)
{
if (thing is Pawn otherPawn && otherPawn != target)
return false;
}
return true;
}
/// <summary>
/// 创建击退飞行器
/// </summary>
private void CreateKnockbackFlyer(Pawn caster, Pawn target, IntVec3 destination)
{
Map map = caster.Map;
// 使用自定义飞行器或默认飞行器
ThingDef flyerDef = Props.knockbackFlyerDef ?? ThingDefOf.PawnFlyer;
// 创建飞行器
PawnFlyer flyer = PawnFlyer.MakeFlyer(
flyerDef,
target,
destination,
Props.flightEffecterDef,
Props.landingSound,
false, // 不携带物品
null, // 不覆盖起始位置
parent, // 传递Ability对象
new LocalTargetInfo(destination)
);
if (flyer != null)
{
GenSpawn.Spawn(flyer, destination, map);
}
}
/// <summary>
/// 检查单元格是否可用于效果
/// </summary>
private bool CanUseCell(Pawn caster, IntVec3 cell)
{
if (caster == null || caster.Map == null)
return false;
if (!cell.InBounds(caster.Map))
return false;
if (!Props.affectCaster && cell == caster.Position)
return false;
if (!Props.canHitFilledCells && cell.Filled(caster.Map))
return false;
if (!cell.InHorDistOf(caster.Position, Props.range))
return false;
return true;
}
/// <summary>
/// 绘制效果预览
/// </summary>
public override void DrawEffectPreview(LocalTargetInfo target)
{
base.DrawEffectPreview(target);
Pawn caster = parent.pawn;
if (caster == null || caster.Map == null || !target.IsValid)
return;
// 绘制扇形区域
List<IntVec3> cells = GetFanShapedCells(caster, target.Cell);
GenDraw.DrawFieldEdges(cells, Color.red);
// 绘制扇形边线
DrawConeBoundaries(caster, target.Cell);
}
/// <summary>
/// 绘制扇形边界线
/// </summary>
private void DrawConeBoundaries(Pawn caster, IntVec3 targetCell)
{
if (caster == null || caster.Map == null)
return;
IntVec3 casterPos = caster.Position;
Vector3 casterVector = casterPos.ToVector3Shifted().Yto0();
// 计算中心线
float horizontalLength = (targetCell - casterPos).LengthHorizontal;
float dirX = (targetCell.x - casterPos.x) / horizontalLength;
float dirZ = (targetCell.z - casterPos.z) / horizontalLength;
IntVec3 clampedTarget = targetCell;
clampedTarget.x = Mathf.RoundToInt(casterPos.x + dirX * Props.range);
clampedTarget.z = Mathf.RoundToInt(casterPos.z + dirZ * Props.range);
float targetAngle = Vector3.SignedAngle(
clampedTarget.ToVector3Shifted().Yto0() - casterVector,
Vector3.right,
Vector3.up);
float halfWidth = Props.lineWidthEnd / 2f;
float coneEdgeDistance = Mathf.Sqrt(
Mathf.Pow((clampedTarget - casterPos).LengthHorizontal, 2f) +
Mathf.Pow(halfWidth, 2f));
float halfAngle = Mathf.Rad2Deg * Mathf.Asin(halfWidth / coneEdgeDistance);
halfAngle = Mathf.Min(halfAngle, Props.coneSizeDegrees / 2f);
// 绘制两条边界线
float leftAngle = targetAngle - halfAngle;
float rightAngle = targetAngle + halfAngle;
Vector3 leftDir = Quaternion.Euler(0, leftAngle, 0) * Vector3.right;
Vector3 rightDir = Quaternion.Euler(0, rightAngle, 0) * Vector3.right;
IntVec3 leftEnd = casterPos + new IntVec3(
Mathf.RoundToInt(leftDir.x * Props.range),
0,
Mathf.RoundToInt(leftDir.z * Props.range)
).ClampInsideMap(caster.Map);
IntVec3 rightEnd = casterPos + new IntVec3(
Mathf.RoundToInt(rightDir.x * Props.range),
0,
Mathf.RoundToInt(rightDir.z * Props.range)
).ClampInsideMap(caster.Map);
GenDraw.DrawLineBetween(casterPos.ToVector3Shifted(), leftEnd.ToVector3Shifted(), SimpleColor.White);
GenDraw.DrawLineBetween(casterPos.ToVector3Shifted(), rightEnd.ToVector3Shifted(), SimpleColor.White);
}
public override bool Valid(LocalTargetInfo target, bool throwMessages = false)
{
if (!base.Valid(target, throwMessages))
return false;
Pawn caster = parent.pawn;
if (caster == null || caster.Map == null)
return false;
// 检查目标是否在范围内
float distance = caster.Position.DistanceTo(target.Cell);
if (distance > Props.range)
{
if (throwMessages)
Messages.Message("AbilityTargetOutOfRange".Translate(), caster, MessageTypeDefOf.RejectInput);
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,58 @@
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
public class CompProperties_AbilityFanShapedStunKnockback : CompProperties_AbilityEffect
{
// 扇形参数
public float range = 5f; // 扇形半径
public float coneSizeDegrees = 45f; // 扇形角度(总角度)
public float lineWidthEnd = 3f; // 扇形末端宽度
// 伤害参数
public DamageDef damageDef = DamageDefOf.Blunt;
public float damageAmount = 15f;
public float armorPenetration = 0f;
// 眩晕参数
public int stunTicks = 180; // 3秒眩晕 (60 ticks = 1秒)
// 击退参数
public int maxKnockbackDistance = 3; // 最大击退距离
public bool canKnockbackIntoWalls = false; // 是否可以击退到墙上
public bool requireLineOfSight = true; // 击退路径是否需要视线
// 新增对非Pawn物体的处理参数
public bool affectNonPawnThings = true; // 是否影响非Pawn物体
public bool canDamageNonPawnThings = true; // 是否可以对非Pawn物体造成伤害
public float nonPawnDamageMultiplier = 1.0f; // 非Pawn物体的伤害倍率
public bool applySpecialEffectsToNonPawn = false; // 是否对非Pawn物体应用特殊效果
// 视觉和音效效果
public EffecterDef impactEffecter; // 命中效果
public SoundDef impactSound; // 命中音效
// 飞行效果设置
public ThingDef knockbackFlyerDef; // 击退飞行器定义
public EffecterDef flightEffecterDef; // 飞行效果
public SoundDef landingSound; // 落地音效
// 过滤设置
public bool affectCaster = false; // 是否影响施法者
public bool canHitFilledCells = true; // 是否可以击中已填充的单元格
public bool onlyAffectEnemies = true; // 只影响敌人
public bool requireLineOfSightToTarget = true; // 是否需要视线到目标
// 近战伤害系数加成
public bool multiplyDamageByMeleeFactor = false; // 伤害是否乘以近战伤害系数
public bool multiplyStunTimeByMeleeFactor = false; // 眩晕时间是否乘以近战伤害系数
public StatDef damageMultiplierStat = null; // 自定义伤害系数Stat如果为空则使用MeleeDamageFactor
public StatDef stunMultiplierStat = null; // 自定义眩晕时间系数Stat如果为空则使用MeleeDamageFactor
public CompProperties_AbilityFanShapedStunKnockback()
{
compClass = typeof(CompAbilityEffect_FanShapedStunKnockback);
}
}
}

View File

@@ -34,6 +34,8 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Compile Include="Abilities\ARA_FanShapedStunKnockback\CompAbilityEffect_FanShapedStunKnockback.cs" />
<Compile Include="Abilities\ARA_FanShapedStunKnockback\CompProperties_AbilityFanShapedStunKnockback.cs" />
<Compile Include="Abilities\ARA_HediffBlacklist\CompAbilityEffect_HediffBlacklist.cs" />
<Compile Include="Abilities\ARA_HediffBlacklist\CompProperties_AbilityHediffBlacklist.cs" />
<Compile Include="Abilities\ARA_HediffGacha\CompAbilityEffect_HediffGacha.cs" />

View File

@@ -1,4 +1,3 @@
// File: Managers/ResearchBlueprintReaderManager.cs
using RimWorld;
using System;
using System.Collections.Generic;
@@ -23,9 +22,18 @@ namespace ArachnaeSwarm
private int cleanupTimer;
private const int CleanupInterval = 2500;
// === 新增:反向校验相关字段 ===
private int reverseCheckTimer;
private const int ReverseCheckInterval = 10000; // 每10秒检查一次6000ticks = 10秒
// === 新增:建筑健康状态记录 ===
private Dictionary<Building_ResearchBlueprintReader, BuildingHealthRecord> buildingHealthRecords;
// === 新增:用于序列化的临时字段 ===
private List<ResearchProjectDef> serializedProjects;
private List<List<Building_ResearchBlueprintReader>> serializedBuildings;
private List<Building_ResearchBlueprintReader> serializedHealthRecordsKeys;
private List<BuildingHealthRecord> serializedHealthRecordsValues;
// === 新增:科技丢失检查计时器 ===
private int lostResearchCheckTimer;
@@ -36,7 +44,9 @@ namespace ArachnaeSwarm
instance = this;
allReaders = new List<Building_ResearchBlueprintReader>();
researchBuildings = new Dictionary<ResearchProjectDef, List<Building_ResearchBlueprintReader>>();
buildingHealthRecords = new Dictionary<Building_ResearchBlueprintReader, BuildingHealthRecord>();
lostResearchCheckTimer = 0;
reverseCheckTimer = 0;
}
public static ResearchBlueprintReaderManager Instance => instance;
@@ -69,19 +79,41 @@ namespace ArachnaeSwarm
Scribe_Collections.Look(ref serializedProjects, "serializedProjects", LookMode.Def);
Scribe_Collections.Look(ref serializedBuildings, "serializedBuildings", LookMode.Reference);
// === 新增:序列化建筑健康记录 ===
serializedHealthRecordsKeys = new List<Building_ResearchBlueprintReader>();
serializedHealthRecordsValues = new List<BuildingHealthRecord>();
foreach (var kvp in buildingHealthRecords)
{
if (kvp.Key != null && !kvp.Key.Destroyed)
{
serializedHealthRecordsKeys.Add(kvp.Key);
serializedHealthRecordsValues.Add(kvp.Value);
}
}
Scribe_Collections.Look(ref serializedHealthRecordsKeys, "healthRecordsKeys", LookMode.Reference);
Scribe_Collections.Look(ref serializedHealthRecordsValues, "healthRecordsValues", LookMode.Deep);
}
else if (Scribe.mode == LoadSaveMode.LoadingVars)
{
// 加载时:清空现有数据
allReaders = new List<Building_ResearchBlueprintReader>();
researchBuildings = new Dictionary<ResearchProjectDef, List<Building_ResearchBlueprintReader>>();
buildingHealthRecords = new Dictionary<Building_ResearchBlueprintReader, BuildingHealthRecord>();
serializedProjects = null;
serializedBuildings = null;
serializedHealthRecordsKeys = null;
serializedHealthRecordsValues = null;
Scribe_Collections.Look(ref serializedProjects, "serializedProjects", LookMode.Def);
Scribe_Collections.Look(ref serializedBuildings, "serializedBuildings", LookMode.Reference);
Scribe_Collections.Look(ref serializedHealthRecordsKeys, "healthRecordsKeys", LookMode.Reference);
Scribe_Collections.Look(ref serializedHealthRecordsValues, "healthRecordsValues", LookMode.Deep);
// 重建 researchBuildings 字典
if (serializedProjects != null && serializedBuildings != null &&
serializedProjects.Count == serializedBuildings.Count)
{
@@ -102,7 +134,7 @@ namespace ArachnaeSwarm
// 添加到所有建筑列表
foreach (var building in buildings)
{
if (!allReaders.Contains(building))
if (building != null && !allReaders.Contains(building))
{
allReaders.Add(building);
}
@@ -112,22 +144,41 @@ namespace ArachnaeSwarm
}
}
ArachnaeLog.Debug($"[ResearchManager] Loaded {allReaders.Count} buildings, {researchBuildings.Count} research projects");
// 重建 buildingHealthRecords 字典
if (serializedHealthRecordsKeys != null && serializedHealthRecordsValues != null &&
serializedHealthRecordsKeys.Count == serializedHealthRecordsValues.Count)
{
for (int i = 0; i < serializedHealthRecordsKeys.Count; i++)
{
var building = serializedHealthRecordsKeys[i];
var record = serializedHealthRecordsValues[i];
if (building != null && record != null)
{
buildingHealthRecords[building] = record;
}
}
}
ArachnaeLog.Debug($"[ResearchManager] Loaded {allReaders.Count} buildings, {researchBuildings.Count} research projects, {buildingHealthRecords.Count} health records");
}
else if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
// 后加载初始化:清理所有无效数据
CleanupInvalidData();
ValidateAllBuildings();
}
// 保存和加载计时器
Scribe_Values.Look(ref lostResearchCheckTimer, "lostResearchCheckTimer", 0);
Scribe_Values.Look(ref reverseCheckTimer, "reverseCheckTimer", 0);
}
public override void GameComponentTick()
{
base.GameComponentTick();
// 正向清理计时器
cleanupTimer++;
if (cleanupTimer >= CleanupInterval)
{
@@ -135,7 +186,15 @@ namespace ArachnaeSwarm
cleanupTimer = 0;
}
// === 新增:定期检查是否有科技因建筑损失而丢失 ===
// 反向校验计时器
reverseCheckTimer++;
if (reverseCheckTimer >= ReverseCheckInterval)
{
PerformReverseValidation();
reverseCheckTimer = 0;
}
// 科技丢失检查计时器
lostResearchCheckTimer++;
if (lostResearchCheckTimer >= LostResearchCheckInterval)
{
@@ -145,50 +204,238 @@ namespace ArachnaeSwarm
}
/// <summary>
/// === 新增:检查建筑损失而丢失的科技 ===
/// === 新增:反向校验 - 管理器主动检查建筑状态 ===
/// </summary>
private void CheckForLostResearch()
private void PerformReverseValidation()
{
if (researchBuildings == null || researchBuildings.Count == 0)
if (allReaders == null || allReaders.Count == 0)
return;
ArachnaeLog.Debug($"[ResearchManager] Checking for lost research projects...");
ArachnaeLog.Debug($"[ResearchManager] Performing reverse validation on {allReaders.Count} buildings");
// 获取需要检查的项目列表(复制以避免修改时遍历)
var projectsToCheck = new List<ResearchProjectDef>(researchBuildings.Keys);
int invalidCount = 0;
int missingCount = 0;
foreach (var project in projectsToCheck)
// 检查所有已注册的建筑
foreach (var building in allReaders.ToList()) // 使用副本避免修改时错误
{
if (project == null)
continue;
if (!researchBuildings.ContainsKey(project))
continue;
var buildings = researchBuildings[project];
if (buildings == null)
if (building == null)
{
researchBuildings.Remove(project);
invalidCount++;
RemoveDeadBuilding(building);
continue;
}
// 清理无效建筑
buildings.RemoveAll(b =>
b == null || b.Destroyed || !b.Spawned || b.Map == null);
// 检查建筑是否仍然存在
bool isValid = ValidateBuildingExistence(building);
// 如果已经没有建筑了
if (buildings.Count == 0)
if (!isValid)
{
missingCount++;
// 更新建筑健康记录
UpdateBuildingHealthRecord(building, false);
// 如果建筑连续多次检查失败,则移除
if (ShouldRemoveBuilding(building))
{
RemoveDeadBuilding(building);
}
}
else
{
// 建筑有效,更新健康记录
UpdateBuildingHealthRecord(building, true);
}
}
if (invalidCount > 0 || missingCount > 0)
{
ArachnaeLog.Debug($"[ResearchManager] Reverse validation found {invalidCount} invalid and {missingCount} missing buildings");
}
}
/// <summary>
/// === 新增:验证建筑是否存在 ===
/// </summary>
private bool ValidateBuildingExistence(Building_ResearchBlueprintReader building)
{
if (building == null)
return false;
try
{
// 方法1检查建筑是否被标记为已摧毁
if (building.Destroyed)
{
ArachnaeLog.Debug($"[ResearchManager] Building {building.ThingID} is marked as destroyed");
return false;
}
// 方法2检查建筑是否在地图上
if (!building.Spawned)
{
ArachnaeLog.Debug($"[ResearchManager] Building {building.ThingID} is not spawned");
return false;
}
// 方法3检查建筑是否有效
if (building.Map == null)
{
ArachnaeLog.Debug($"[ResearchManager] Building {building.ThingID} has no map reference");
return false;
}
// 方法4检查建筑是否在地图建筑列表中
if (building.Map != null && !building.Map.listerBuildings.allBuildingsColonist.Contains(building))
{
// 建筑可能被取消、拆除或移动
ArachnaeLog.Debug($"[ResearchManager] Building {building.ThingID} not found in map building list");
return false;
}
return true;
}
catch (Exception ex)
{
ArachnaeLog.Debug($"[ResearchManager] Error validating building {building.ThingID}: {ex.Message}");
return false;
}
}
/// <summary>
/// === 新增:更新建筑健康记录 ===
/// </summary>
private void UpdateBuildingHealthRecord(Building_ResearchBlueprintReader building, bool isHealthy)
{
if (building == null)
return;
if (!buildingHealthRecords.ContainsKey(building))
{
buildingHealthRecords[building] = new BuildingHealthRecord();
}
var record = buildingHealthRecords[building];
record.LastCheckTick = Find.TickManager.TicksGame;
record.IsHealthy = isHealthy;
if (isHealthy)
{
record.ConsecutiveFailures = 0;
record.LastHealthyTick = Find.TickManager.TicksGame;
}
else
{
record.ConsecutiveFailures++;
ArachnaeLog.Debug($"[ResearchManager] Building {building.ThingID} health check failed {record.ConsecutiveFailures} times");
}
}
/// <summary>
/// === 新增:判断是否应该移除建筑 ===
/// </summary>
private bool ShouldRemoveBuilding(Building_ResearchBlueprintReader building)
{
if (building == null || !buildingHealthRecords.ContainsKey(building))
return true;
var record = buildingHealthRecords[building];
// 如果连续失败超过3次或者超过30秒没有健康状态
if (record.ConsecutiveFailures >= 3)
{
ArachnaeLog.Debug($"[ResearchManager] Building {building.ThingID} has {record.ConsecutiveFailures} consecutive failures, removing");
return true;
}
if (record.LastHealthyTick > 0 &&
Find.TickManager.TicksGame - record.LastHealthyTick > 1800) // 30秒
{
ArachnaeLog.Debug($"[ResearchManager] Building {building.ThingID} has been unhealthy for 30+ seconds, removing");
return true;
}
return false;
}
/// <summary>
/// === 新增:移除死亡建筑 ===
/// </summary>
private void RemoveDeadBuilding(Building_ResearchBlueprintReader building)
{
if (building == null)
return;
ArachnaeLog.Debug($"[ResearchManager] Removing dead building: {building.ThingID}");
// 从所有列表中移除
allReaders.Remove(building);
buildingHealthRecords.Remove(building);
// 从研究项目中移除
var project = building.StoredResearch;
if (project != null && researchBuildings.ContainsKey(project))
{
researchBuildings[project].Remove(building);
// 如果项目没有建筑了,检查是否丢失
if (researchBuildings[project].Count == 0)
{
researchBuildings.Remove(project);
// 调用Patch创建的移除方法来丢失科技
OnResearchLostDueToBuildingLoss(project);
}
}
}
/// <summary>
/// === 新增:科技因建筑损失而丢失的处理 ===
/// === 新增:加载后验证所有建筑 ===
/// </summary>
private void ValidateAllBuildings()
{
ArachnaeLog.Debug("[ResearchManager] Validating all buildings after load");
// 验证所有已注册的建筑
foreach (var building in allReaders.ToList())
{
if (!ValidateBuildingExistence(building))
{
ArachnaeLog.Debug($"[ResearchManager] Invalid building detected after load: {building.ThingID}");
RemoveDeadBuilding(building);
}
else
{
UpdateBuildingHealthRecord(building, true);
}
}
// 清理无效的建筑健康记录
var invalidRecords = buildingHealthRecords.Keys.Where(b => b == null || b.Destroyed).ToList();
foreach (var building in invalidRecords)
{
buildingHealthRecords.Remove(building);
}
ArachnaeLog.Debug($"[ResearchManager] Validation complete: {allReaders.Count} valid buildings, {buildingHealthRecords.Count} health records");
}
/// <summary>
/// 检查因建筑损失而丢失的科技
/// </summary>
private void CheckForLostResearch()
{
if (researchBuildings == null || researchBuildings.Count == 0)
return;
// 只记录调试信息,不重复执行
if (Find.TickManager.TicksGame % 60000 == 0) // 每分钟记录一次
{
ArachnaeLog.Debug($"[ResearchManager] Research status: {researchBuildings.Count} active projects");
}
}
/// <summary>
/// 科技因建筑损失而丢失的处理
/// </summary>
private void OnResearchLostDueToBuildingLoss(ResearchProjectDef project)
{
@@ -233,7 +480,7 @@ namespace ArachnaeSwarm
if (allReaders != null)
{
removedCount += allReaders.RemoveAll(b =>
b == null || b.Destroyed || !b.Spawned || b.Map == null);
b == null || b.Destroyed);
if (removedCount > 0)
{
@@ -260,7 +507,7 @@ namespace ArachnaeSwarm
// 清理无效建筑
kvp.Value.RemoveAll(b =>
b == null || b.Destroyed || !b.Spawned || b.Map == null);
b == null || b.Destroyed);
if (kvp.Value.Count == 0)
{
@@ -289,6 +536,20 @@ namespace ArachnaeSwarm
{
researchBuildings = new Dictionary<ResearchProjectDef, List<Building_ResearchBlueprintReader>>();
}
// 清理建筑健康记录
if (buildingHealthRecords != null)
{
var keysToRemove = buildingHealthRecords.Keys.Where(k => k == null || k.Destroyed).ToList();
foreach (var key in keysToRemove)
{
buildingHealthRecords.Remove(key);
}
}
else
{
buildingHealthRecords = new Dictionary<Building_ResearchBlueprintReader, BuildingHealthRecord>();
}
}
/// <summary>
@@ -296,7 +557,7 @@ namespace ArachnaeSwarm
/// </summary>
public void RegisterReader(Building_ResearchBlueprintReader reader)
{
if (reader == null || reader.Destroyed || !reader.Spawned)
if (reader == null || reader.Destroyed)
return;
// 防止重复注册
@@ -308,6 +569,19 @@ namespace ArachnaeSwarm
if (!allReaders.Contains(reader))
{
allReaders.Add(reader);
// 初始化健康记录
if (!buildingHealthRecords.ContainsKey(reader))
{
buildingHealthRecords[reader] = new BuildingHealthRecord
{
LastHealthyTick = Find.TickManager.TicksGame,
LastCheckTick = Find.TickManager.TicksGame,
IsHealthy = true,
ConsecutiveFailures = 0
};
}
ArachnaeLog.Debug($"[ResearchManager] Registered reader: {reader.ThingID} at position {reader.Position}");
}
}
@@ -370,10 +644,34 @@ namespace ArachnaeSwarm
{
allReaders.Remove(building);
}
// 从健康记录中移除
if (buildingHealthRecords != null)
{
buildingHealthRecords.Remove(building);
}
}
/// <summary>
/// === 新增:获取指定科技的建筑数量 ===
/// === 新增:建筑健康心跳 - 建筑主动报告 ===
/// </summary>
public void ReportBuildingHealth(Building_ResearchBlueprintReader building)
{
if (building == null)
return;
if (buildingHealthRecords.ContainsKey(building))
{
var record = buildingHealthRecords[building];
record.LastHealthyTick = Find.TickManager.TicksGame;
record.LastCheckTick = Find.TickManager.TicksGame;
record.IsHealthy = true;
record.ConsecutiveFailures = 0;
}
}
/// <summary>
/// 获取指定科技的建筑数量
/// </summary>
public int GetBuildingCountForResearch(ResearchProjectDef project)
{
@@ -384,37 +682,12 @@ namespace ArachnaeSwarm
}
/// <summary>
/// === 新增:手动触发科技丢失检查(用于调试) ===
/// === 新增:手动触发反向校验(用于调试) ===
/// </summary>
public void DebugTriggerLostResearchCheck()
public void DebugTriggerReverseValidation()
{
ArachnaeLog.Debug("[ResearchManager] Manual trigger of lost research check");
CheckForLostResearch();
}
/// <summary>
/// === 新增:强制移除某个科技(用于调试) ===
/// </summary>
public void DebugForceRemoveResearch(ResearchProjectDef project)
{
if (project == null)
return;
ArachnaeLog.Debug($"[ResearchManager] Debug force remove research: {project.defName}");
// 从字典中移除
if (researchBuildings.ContainsKey(project))
{
researchBuildings.Remove(project);
}
// 使用ResearchRemover移除科技
ResearchRemover.RemoveResearchProject(project, removeDependencies: false);
Messages.Message(
$"Debug: Research '{project.LabelCap}' has been forcibly removed.",
MessageTypeDefOf.NeutralEvent
);
ArachnaeLog.Debug("[ResearchManager] Manual trigger of reverse validation");
PerformReverseValidation();
}
/// <summary>
@@ -431,6 +704,7 @@ namespace ArachnaeSwarm
ArachnaeLog.Debug("=== Research Manager Status ===");
ArachnaeLog.Debug($"Total buildings: {Instance.allReaders?.Count ?? 0}");
ArachnaeLog.Debug($"Active research projects: {Instance.researchBuildings?.Count ?? 0}");
ArachnaeLog.Debug($"Building health records: {Instance.buildingHealthRecords?.Count ?? 0}");
if (Instance.researchBuildings != null)
{
@@ -442,6 +716,39 @@ namespace ArachnaeSwarm
ArachnaeLog.Debug($" - {kvp.Key.defName}: {activeBuildings} active buildings");
}
}
// 显示建筑健康状态
if (Instance.buildingHealthRecords != null && Instance.buildingHealthRecords.Count > 0)
{
ArachnaeLog.Debug("=== Building Health Status ===");
foreach (var kvp in Instance.buildingHealthRecords)
{
if (kvp.Key == null) continue;
string status = kvp.Value.IsHealthy ? "Healthy" : "Unhealthy";
string timeSinceHealthy = (Find.TickManager.TicksGame - kvp.Value.LastHealthyTick).ToStringTicksToPeriod();
ArachnaeLog.Debug($" - {kvp.Key.ThingID}: {status}, Failures: {kvp.Value.ConsecutiveFailures}, Last healthy: {timeSinceHealthy} ago");
}
}
}
}
/// <summary>
/// === 新增:建筑健康记录类 ===
/// </summary>
public class BuildingHealthRecord : IExposable
{
public int LastCheckTick = 0;
public int LastHealthyTick = 0;
public bool IsHealthy = false;
public int ConsecutiveFailures = 0;
public void ExposeData()
{
Scribe_Values.Look(ref LastCheckTick, "LastCheckTick", 0);
Scribe_Values.Look(ref LastHealthyTick, "LastHealthyTick", 0);
Scribe_Values.Look(ref IsHealthy, "IsHealthy", false);
Scribe_Values.Look(ref ConsecutiveFailures, "ConsecutiveFailures", 0);
}
}
}