1
This commit is contained in:
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user