diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll
index 17f4758..5b2f760 100644
Binary files a/1.6/1.6/Assemblies/ArachnaeSwarm.dll and b/1.6/1.6/Assemblies/ArachnaeSwarm.dll differ
diff --git a/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon.xml b/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon.xml
index 027e2aa..6eb3a16 100644
--- a/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon.xml
+++ b/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon.xml
@@ -420,7 +420,7 @@
false
0.8
Bullet_ARA_RW_Basic_Fist_Needle_Gun
- 22
+ 28
32
2
SpitterSpit
@@ -498,7 +498,7 @@
false
0.8
Bullet_ARA_RW_Basic_Fist_Needle_Gun_SG
- 22
+ 28
3
12
SpitterSpit
diff --git a/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon_FireSpew.xml b/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon_FireSpew.xml
deleted file mode 100644
index f36405e..0000000
--- a/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon_FireSpew.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/1.6/1.6/Defs/Thing_building/ARA_SwarmTurret.xml b/1.6/1.6/Defs/Thing_building/ARA_SwarmTurret.xml
index 8d0ff6a..6370358 100644
--- a/1.6/1.6/Defs/Thing_building/ARA_SwarmTurret.xml
+++ b/1.6/1.6/Defs/Thing_building/ARA_SwarmTurret.xml
@@ -270,7 +270,7 @@
ARA_AutoSniperCannon
阿拉克涅虫族用于防御巢穴的组织之一。它由大量没有自主意识的高度特化器官共同构成,能以较高射速连续地发射棘刺,重创大型敌人或来犯集群。作为阿拉克涅防御组织,其可以通过获取营养来自行生成补充弹药。
- Building_TurretGun
+ ArachnaeSwarm.Building_TurretGunHasSpeed
MapMeshAndRealTime
true
true
@@ -345,6 +345,11 @@
+
+
+ 0.5
+
+
260
0
@@ -387,7 +392,7 @@
- Verb_Shoot
+ ArachnaeSwarm.Verb_ShootWithOffset
true
ARA_Bullet_SniperCannon
0.1
@@ -401,13 +406,20 @@
1
+
+
+
+ (0, -2.5)
+
+
+
ARA_Acidling_AutoMortar
阿拉克涅虫族用于防御巢穴的组织之一。它由大量没有自主意识的高度特化器官共同构成,能向进犯巢穴的敌军发射多只阿拉克涅酸爆种辅虫,这些辅虫将自行寻找目标以发起自杀性袭击。作为阿拉克涅防御组织,其可以通过获取营养来自行生成补充弹药。
- Building_TurretGun
+ ArachnaeSwarm.Building_TurretGunHasSpeed
MapMeshAndRealTime
true
true
@@ -482,6 +494,11 @@
+
+
+ 0.5
+
+
260
0
@@ -522,7 +539,7 @@
- Verb_Shoot
+ ArachnaeSwarm.Verb_ShootWithOffset
ArachnaeBase_Race_Acidling_Proj
3
2
@@ -539,6 +556,13 @@
+
+
+
+ (0, -1.4)
+
+
+
ArachnaeBase_Race_Acidling_Proj
@@ -674,7 +698,7 @@
- Verb_Shoot
+ ArachnaeSwarm.Verb_ShootWithOffset
true
Projectile_CatastropheMissile
1
@@ -692,6 +716,13 @@
+
+
+
+ (0, -5)
+
+
+
ARA_CatastropheMissile_Shell
diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj
index aff9a51..61d33ac 100644
--- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj
+++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj
@@ -97,6 +97,8 @@
+
+
diff --git a/Source/ArachnaeSwarm/Buildings/Building_TurretGunHasSpeed.cs b/Source/ArachnaeSwarm/Buildings/Building_TurretGunHasSpeed.cs
new file mode 100644
index 0000000..65a6197
--- /dev/null
+++ b/Source/ArachnaeSwarm/Buildings/Building_TurretGunHasSpeed.cs
@@ -0,0 +1,924 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using RimWorld;
+using UnityEngine;
+using Verse;
+using Verse.AI;
+using Verse.AI.Group;
+
+namespace ArachnaeSwarm
+{
+ public class ModExt_HasSpeedTurret : DefModExtension
+ {
+ public float speed = 1f;
+ }
+ ///
+ /// 非瞬时瞄准的炮塔建筑类
+ /// 继承自原版炮塔,增加了平滑旋转瞄准功能
+ ///
+ public class Building_TurretGunHasSpeed : Building_TurretGun
+ {
+ // 当前炮塔角度
+ public float curAngle;
+
+ ///
+ /// 旋转速度属性
+ /// 从Mod扩展配置中获取旋转速度,如果没有配置则使用默认值1f
+ ///
+ public float rotateSpeed
+ {
+ get
+ {
+ ModExt_HasSpeedTurret ext = this.ext;
+ return ext.speed;
+ }
+ }
+ ///
+ /// Mod扩展配置属性
+ /// 获取炮塔定义的Mod扩展配置
+ ///
+ public ModExt_HasSpeedTurret ext
+ {
+ get
+ {
+ return this.def.GetModExtension();
+ }
+ }
+
+ ///
+ /// 炮塔方向向量
+ /// 根据当前角度计算炮塔的朝向向量
+ ///
+ public Vector3 turretOrientation
+ {
+ get
+ {
+ return Vector3.forward.RotatedBy(this.curAngle);
+ }
+ }
+
+ ///
+ /// 目标角度差
+ /// 计算当前炮塔方向与目标方向之间的角度差
+ ///
+ public float deltaAngle
+ {
+ get
+ {
+ return (this.currentTargetInt == null) ? 0f : Vector3.SignedAngle(this.turretOrientation, (this.currentTargetInt.CenterVector3 - this.DrawPos).Yto0(), Vector3.up);
+ }
+ }
+
+ ///
+ /// 数据保存和加载
+ /// 重写ExposeData以保存和加载当前角度数据
+ ///
+ public override void ExposeData()
+ {
+ base.ExposeData();
+ Scribe_Values.Look(ref this.curAngle, "curAngle", 0f, false);
+ }
+
+ ///
+ /// 检查是否可以攻击目标(LocalTargetInfo重载)
+ ///
+ /// 目标信息
+ /// 是否可以攻击
+ private bool CanAttackTarget(LocalTargetInfo t)
+ {
+ return this.CanAttackTarget(t.CenterVector3);
+ }
+
+ ///
+ /// 检查是否可以攻击目标(Thing重载)
+ ///
+ /// 目标物体
+ /// 是否可以攻击
+ private bool CanAttackTarget(Thing t)
+ {
+ return this.CanAttackTarget(t.DrawPos);
+ }
+
+ ///
+ /// 检查是否可以攻击目标(Vector3重载)
+ /// 判断目标是否在当前炮塔的瞄准范围内
+ ///
+ /// 目标位置
+ /// 是否可以攻击
+ private bool CanAttackTarget(Vector3 t)
+ {
+ return Vector3.Angle(this.turretOrientation, (t - this.DrawPos).Yto0()) <= this.rotateSpeed;
+ }
+
+ ///
+ /// 每帧更新
+ /// 处理炮塔的旋转逻辑
+ ///
+ protected override void Tick()
+ {
+ // 如果炮塔处于激活状态且有目标
+ if (base.Active && this.currentTargetInt != null)
+ {
+ // 如果准备开火但角度差过大,延迟开火
+ if (this.burstWarmupTicksLeft == 1 && Mathf.Abs(this.deltaAngle) > this.rotateSpeed)
+ {
+ this.burstWarmupTicksLeft++;
+ }
+
+ // 根据角度差更新当前角度
+ this.curAngle += ((Mathf.Abs(this.deltaAngle) - this.rotateSpeed > 0f) ?
+ (Mathf.Sign(this.deltaAngle) * this.rotateSpeed) : this.deltaAngle);
+ }
+
+ base.Tick();
+ // 规范化角度值到0-360度范围
+ this.curAngle = this.Trim(this.curAngle);
+ }
+
+ ///
+ /// 角度规范化
+ /// 将角度值限制在0-360度范围内
+ ///
+ /// 输入角度
+ /// 规范化后的角度
+ protected float Trim(float angle)
+ {
+ if (angle > 360f)
+ {
+ angle -= 360f;
+ }
+ if (angle < 0f)
+ {
+ angle += 360f;
+ }
+ return angle;
+ }
+
+ ///
+ /// 绘制炮塔
+ /// 设置炮塔顶部的旋转角度
+ ///
+ /// 绘制位置
+ /// 是否翻转
+ protected override void DrawAt(Vector3 drawLoc, bool flip = false)
+ {
+ this.top.CurRotation = this.curAngle;
+ base.DrawAt(drawLoc, flip);
+ }
+
+ ///
+ /// 获取目标搜索器
+ /// 如果有人操作则返回操作者,否则返回炮塔自身
+ ///
+ /// 目标搜索器
+ private IAttackTargetSearcher TargSearcher()
+ {
+ if (this.mannableComp != null && this.mannableComp.MannedNow)
+ {
+ return this.mannableComp.ManningPawn;
+ }
+ else
+ {
+ return this;
+ }
+ }
+
+ ///
+ /// 检查目标是否有效
+ /// 过滤不适合攻击的目标
+ ///
+ /// 目标物体
+ /// 目标是否有效
+ private bool IsValidTarget(Thing t)
+ {
+ Pawn pawn = t as Pawn;
+ if (pawn != null)
+ {
+ // 玩家派系的炮塔不攻击囚犯
+ if (base.Faction == Faction.OfPlayer && pawn.IsPrisoner)
+ {
+ return false;
+ }
+
+ // 检查弹道是否会被厚屋顶阻挡
+ if (this.AttackVerb.ProjectileFliesOverhead())
+ {
+ RoofDef roofDef = base.Map.roofGrid.RoofAt(t.Position);
+ if (roofDef != null && roofDef.isThickRoof)
+ {
+ return false;
+ }
+ }
+
+ // 无人操作的机械炮塔不攻击友好机械单位
+ if (this.mannableComp == null)
+ {
+ return !GenAI.MachinesLike(base.Faction, pawn);
+ }
+
+ // 有人操作的炮塔不攻击玩家动物
+ if (pawn.RaceProps.Animal && pawn.Faction == Faction.OfPlayer)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// 尝试寻找新目标
+ /// 重写目标选择逻辑,支持角度限制
+ ///
+ /// 新的目标信息
+ public override LocalTargetInfo TryFindNewTarget()
+ {
+ IAttackTargetSearcher attackTargetSearcher = this.TargSearcher();
+ Faction faction = attackTargetSearcher.Thing.Faction;
+ float range = this.AttackVerb.verbProps.range;
+
+ Building t;
+ // 50%概率优先攻击殖民者建筑(如果敌对且使用抛射武器)
+ if (Rand.Value < 0.5f && this.AttackVerb.ProjectileFliesOverhead() &&
+ faction.HostileTo(Faction.OfPlayer) &&
+ base.Map.listerBuildings.allBuildingsColonist.Where(delegate (Building x)
+ {
+ float minRange = this.AttackVerb.verbProps.EffectiveMinRange(x, this);
+ float distanceSquared = (float)x.Position.DistanceToSquared(this.Position);
+ return distanceSquared > minRange * minRange && distanceSquared < range * range;
+ }).TryRandomElement(out t))
+ {
+ return t;
+ }
+ else
+ {
+ // 设置目标扫描标志
+ TargetScanFlags targetScanFlags = TargetScanFlags.NeedThreat | TargetScanFlags.NeedAutoTargetable;
+
+ if (!this.AttackVerb.ProjectileFliesOverhead())
+ {
+ targetScanFlags |= TargetScanFlags.NeedLOSToAll;
+ targetScanFlags |= TargetScanFlags.LOSBlockableByGas;
+ }
+
+ if (this.AttackVerb.IsIncendiary_Ranged())
+ {
+ targetScanFlags |= TargetScanFlags.NeedNonBurning;
+ }
+
+ if (this.def.building.IsMortar)
+ {
+ targetScanFlags |= TargetScanFlags.NeedNotUnderThickRoof;
+ }
+
+ // 使用角度感知的目标查找器
+ return (Thing)AttackTargetFinderAngle.BestShootTargetFromCurrentPosition(
+ attackTargetSearcher, targetScanFlags, this.turretOrientation,
+ new Predicate(this.IsValidTarget), 0f, 9999f);
+ }
+ }
+ }
+ ///
+ /// 攻击目标查找器(角度优化版)
+ /// 提供基于角度优化的攻击目标选择功能
+ ///
+ public static class AttackTargetFinderAngle
+ {
+ // 友军误伤评分偏移量常量
+ private const float FriendlyFireScoreOffsetPerHumanlikeOrMechanoid = 18f; // 每人类或机械族的友军误伤分数偏移
+ private const float FriendlyFireScoreOffsetPerAnimal = 7f; // 每动物的友军误伤分数偏移
+ private const float FriendlyFireScoreOffsetPerNonPawn = 10f; // 每非pawn单位的友军误伤分数偏移
+ private const float FriendlyFireScoreOffsetSelf = 40f; // 对自己造成误伤的分数偏移
+ // 临时目标列表,用于缓存计算过程中的目标
+ private static List tmpTargets = new List(128);
+
+ // 可用射击目标及其分数的列表
+ private static List> availableShootingTargets = new List>();
+
+ // 临时存储目标分数的列表
+ private static List tmpTargetScores = new List();
+
+ // 临时存储是否可以向目标射击的列表
+ private static List tmpCanShootAtTarget = new List();
+ ///
+ /// 从当前位置寻找最佳射击目标
+ ///
+ /// 搜索者(攻击目标搜索器)
+ /// 目标扫描标志
+ /// 射击角度
+ /// 目标验证器(可选)
+ /// 最小距离(默认0)
+ /// 最大距离(默认9999)
+ /// 最佳攻击目标,如果没有则返回null
+ public static IAttackTarget BestShootTargetFromCurrentPosition(
+ IAttackTargetSearcher searcher,
+ TargetScanFlags flags,
+ Vector3 angle,
+ Predicate validator = null,
+ float minDistance = 0f,
+ float maxDistance = 9999f)
+ {
+ // 获取当前有效动词(武器)
+ Verb currentEffectiveVerb = searcher.CurrentEffectiveVerb;
+
+ // 检查是否有攻击动词
+ if (currentEffectiveVerb == null)
+ {
+ Log.Error("BestShootTargetFromCurrentPosition with " + searcher.ToStringSafe() + " who has no attack verb.");
+ return null;
+ }
+
+ // 计算实际的最小和最大距离,考虑武器的属性
+ float actualMinDistance = Mathf.Max(minDistance, currentEffectiveVerb.verbProps.minRange);
+ float actualMaxDistance = Mathf.Min(maxDistance, currentEffectiveVerb.verbProps.range);
+
+ // 调用主要的目标查找方法
+ return BestAttackTarget(
+ searcher,
+ flags,
+ angle,
+ validator,
+ actualMinDistance,
+ actualMaxDistance,
+ default(IntVec3),
+ float.MaxValue,
+ false);
+ }
+
+ ///
+ /// 查找最佳攻击目标(核心方法)
+ ///
+ /// 搜索者
+ /// 目标扫描标志
+ /// 射击角度
+ /// 目标验证器
+ /// 最小距离
+ /// 最大距离
+ /// 搜索中心点
+ /// 从中心点的最大移动半径
+ /// 是否可以攻击比有效最小距离更近的目标
+ /// 最佳攻击目标
+ public static IAttackTarget BestAttackTarget(
+ IAttackTargetSearcher searcher,
+ TargetScanFlags flags,
+ Vector3 angle,
+ Predicate validator = null,
+ float minDist = 0f,
+ float maxDist = 9999f,
+ IntVec3 locus = default(IntVec3),
+ float maxTravelRadiusFromLocus = float.MaxValue,
+ bool canTakeTargetsCloserThanEffectiveMinRange = true)
+ {
+ // 获取搜索者的Thing对象和当前有效动词
+ Thing searcherThing = searcher.Thing;
+ Verb verb = searcher.CurrentEffectiveVerb;
+
+ // 验证攻击动词是否存在
+ if (verb == null)
+ {
+ Log.Error("BestAttackTarget with " + searcher.ToStringSafe() + " who has no attack verb.");
+ return null;
+ }
+
+ // 初始化各种标志和参数
+ bool onlyTargetMachines = verb.IsEMP(); // 是否只瞄准机械单位(EMP武器)
+ float minDistSquared = minDist * minDist; // 最小距离的平方(用于距离比较优化)
+
+ // 计算从搜索中心点的最大距离平方
+ float maxLocusDist = maxTravelRadiusFromLocus + verb.verbProps.range;
+ float maxLocusDistSquared = maxLocusDist * maxLocusDist;
+
+ // LOS(视线)验证器,用于检查是否被烟雾阻挡
+ Predicate losValidator = null;
+ if ((flags & TargetScanFlags.LOSBlockableByGas) > TargetScanFlags.None)
+ {
+ losValidator = (IntVec3 vec3) => !vec3.AnyGas(searcherThing.Map, GasType.BlindSmoke);
+ }
+
+ // 获取潜在目标列表
+ tmpTargets.Clear();
+ tmpTargets.AddRange(searcherThing.Map.attackTargetsCache.GetPotentialTargetsFor(searcher));
+
+ // 移除非战斗人员(根据标志)
+ tmpTargets.RemoveAll(t => ShouldIgnoreNoncombatant(searcherThing, t, flags));
+
+ // 内部验证器函数
+ bool InnerValidator(IAttackTarget target, Predicate losValidator)
+ {
+ Thing targetThing = target.Thing;
+ if (target == searcher)
+ {
+ return false;
+ }
+
+ if (minDistSquared > 0f && (float)(searcherThing.Position - targetThing.Position).LengthHorizontalSquared < minDistSquared)
+ {
+ return false;
+ }
+
+ if (!canTakeTargetsCloserThanEffectiveMinRange)
+ {
+ float num3 = verb.verbProps.EffectiveMinRange(targetThing, searcherThing);
+ if (num3 > 0f && (float)(searcherThing.Position - targetThing.Position).LengthHorizontalSquared < num3 * num3)
+ {
+ return false;
+ }
+ }
+
+ if (maxTravelRadiusFromLocus < 9999f && (float)(targetThing.Position - locus).LengthHorizontalSquared > maxLocusDistSquared)
+ {
+ return false;
+ }
+
+ if (!searcherThing.HostileTo(targetThing))
+ {
+ return false;
+ }
+
+ if (validator != null && !validator(targetThing))
+ {
+ return false;
+ }
+
+
+ if ((flags & TargetScanFlags.NeedNotUnderThickRoof) != 0)
+ {
+ RoofDef roof = targetThing.Position.GetRoof(targetThing.Map);
+ if (roof != null && roof.isThickRoof)
+ {
+ return false;
+ }
+ }
+
+ if ((flags & TargetScanFlags.NeedLOSToAll) != 0)
+ {
+ if (losValidator != null && (!losValidator(searcherThing.Position) || !losValidator(targetThing.Position)))
+ {
+ return false;
+ }
+
+ if (!searcherThing.CanSee(targetThing))
+ {
+ if (target is Pawn)
+ {
+ if ((flags & TargetScanFlags.NeedLOSToPawns) != 0)
+ {
+ return false;
+ }
+ }
+ else if ((flags & TargetScanFlags.NeedLOSToNonPawns) != 0)
+ {
+ return false;
+ }
+ }
+ }
+
+ if (((flags & TargetScanFlags.NeedThreat) != 0 || (flags & TargetScanFlags.NeedAutoTargetable) != 0) && target.ThreatDisabled(searcher))
+ {
+ return false;
+ }
+
+ if ((flags & TargetScanFlags.NeedAutoTargetable) != 0 && !AttackTargetFinder.IsAutoTargetable(target))
+ {
+ return false;
+ }
+
+ if ((flags & TargetScanFlags.NeedActiveThreat) != 0 && !GenHostility.IsActiveThreatTo(target, searcher.Thing.Faction))
+ {
+ return false;
+ }
+
+ Pawn pawn = target as Pawn;
+ if (onlyTargetMachines && pawn != null && pawn.RaceProps.IsFlesh)
+ {
+ return false;
+ }
+
+ if ((flags & TargetScanFlags.NeedNonBurning) != 0 && targetThing.IsBurning())
+ {
+ return false;
+ }
+
+ if (searcherThing.def.race != null && (int)searcherThing.def.race.intelligence >= 2)
+ {
+ CompExplosive compExplosive = targetThing.TryGetComp();
+ if (compExplosive != null && compExplosive.wickStarted)
+ {
+ return false;
+ }
+ }
+
+ // 距离验证
+ if (!targetThing.Position.InHorDistOf(searcherThing.Position, maxDist))
+ return false;
+
+ // 最小距离验证
+ if (!canTakeTargetsCloserThanEffectiveMinRange &&
+ (float)(searcherThing.Position - targetThing.Position).LengthHorizontalSquared < minDistSquared)
+ return false;
+
+ // 中心点距离验证
+ if (locus.IsValid &&
+ (float)(locus - targetThing.Position).LengthHorizontalSquared > maxLocusDistSquared)
+ return false;
+
+ // 自定义验证器
+ if (validator != null && !validator(targetThing))
+ return false;
+
+ return true;
+ }
+
+ // 检查是否有可以直接射击的目标
+ bool hasDirectShootTarget = false;
+ for (int i = 0; i < tmpTargets.Count; i++)
+ {
+ IAttackTarget attackTarget = tmpTargets[i];
+ if (attackTarget.Thing.Position.InHorDistOf(searcherThing.Position, maxDist) &&
+ InnerValidator(attackTarget, losValidator) &&
+ CanShootAtFromCurrentPosition(attackTarget, searcher, verb))
+ {
+ hasDirectShootTarget = true;
+ break;
+ }
+ }
+
+ IAttackTarget bestTarget;
+
+ if (hasDirectShootTarget)
+ {
+ // 如果有可以直接射击的目标,使用基于分数的随机选择
+ tmpTargets.RemoveAll(x => !x.Thing.Position.InHorDistOf(searcherThing.Position, maxDist) || !InnerValidator(x, losValidator));
+ bestTarget = GetRandomShootingTargetByScore(tmpTargets, searcher, verb, angle);
+ }
+ else
+ {
+ // 否则使用最近的目标选择策略
+ bool needReachableIfCantHit = (flags & TargetScanFlags.NeedReachableIfCantHitFromMyPos) > TargetScanFlags.None;
+ bool needReachable = (flags & TargetScanFlags.NeedReachable) > TargetScanFlags.None;
+
+ Predicate reachableValidator;
+ if (!needReachableIfCantHit || needReachable)
+ {
+ reachableValidator = (Thing t) => InnerValidator((IAttackTarget)t, losValidator);
+ }
+ else
+ {
+ reachableValidator = (Thing t) => InnerValidator((IAttackTarget)t, losValidator) &&
+ CanShootAtFromCurrentPosition((IAttackTarget)t, searcher, verb);
+ }
+
+ bestTarget = (IAttackTarget)GenClosest.ClosestThing_Global(
+ searcherThing.Position,
+ tmpTargets,
+ maxDist,
+ reachableValidator,
+ null,
+ false);
+ }
+
+ tmpTargets.Clear();
+ return bestTarget;
+ }
+ ///
+ /// 检查是否应该忽略非战斗人员
+ ///
+ private static bool ShouldIgnoreNoncombatant(Thing searcherThing, IAttackTarget target, TargetScanFlags flags)
+ {
+ // 只对Pawn类型的目标进行判断
+ if (!(target is Pawn pawn))
+ return false;
+
+ // 如果是战斗人员,不忽略
+ if (pawn.IsCombatant())
+ return false;
+
+ // 如果设置了忽略非战斗人员标志,则忽略
+ if ((flags & TargetScanFlags.IgnoreNonCombatants) > TargetScanFlags.None)
+ return true;
+
+ // 如果看不到非战斗人员,则忽略
+ return !GenSight.LineOfSightToThing(searcherThing.Position, pawn, searcherThing.Map, false, null);
+ }
+
+ ///
+ /// 检查是否可以从当前位置射击目标
+ ///
+ private static bool CanShootAtFromCurrentPosition(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb)
+ {
+ return verb != null && verb.CanHitTargetFrom(searcher.Thing.Position, target.Thing);
+ }
+
+ ///
+ /// 通过权重随机获取射击目标
+ ///
+ private static IAttackTarget GetRandomShootingTargetByScore(List targets, IAttackTargetSearcher searcher, Verb verb, Vector3 angle)
+ {
+ var availableTargets = GetAvailableShootingTargetsByScore(targets, searcher, verb, angle);
+ if (availableTargets.TryRandomElementByWeight(x => x.Second, out Pair result))
+ {
+ return result.First;
+ }
+ return null;
+ }
+
+ ///
+ /// 获取可用射击目标及其分数的列表
+ ///
+ private static List> GetAvailableShootingTargetsByScore(
+ List rawTargets,
+ IAttackTargetSearcher searcher,
+ Verb verb,
+ Vector3 angle)
+ {
+ availableShootingTargets.Clear();
+
+ if (rawTargets.Count == 0)
+ return availableShootingTargets;
+
+ // 初始化临时列表
+ tmpTargetScores.Clear();
+ tmpCanShootAtTarget.Clear();
+
+ float highestScore = float.MinValue;
+ IAttackTarget bestTarget = null;
+
+ // 第一轮遍历:计算基础分数并标记可射击目标
+ for (int i = 0; i < rawTargets.Count; i++)
+ {
+ tmpTargetScores.Add(float.MinValue);
+ tmpCanShootAtTarget.Add(false);
+
+ // 跳过搜索者自身
+ if (rawTargets[i] == searcher)
+ continue;
+
+ // 检查是否可以射击
+ bool canShoot = CanShootAtFromCurrentPosition(rawTargets[i], searcher, verb);
+ tmpCanShootAtTarget[i] = canShoot;
+
+ if (canShoot)
+ {
+ // 计算射击目标分数
+ float score = GetShootingTargetScore(rawTargets[i], searcher, verb, angle);
+ tmpTargetScores[i] = score;
+
+ // 更新最佳目标
+ if (bestTarget == null || score > highestScore)
+ {
+ bestTarget = rawTargets[i];
+ highestScore = score;
+ }
+ }
+ }
+
+ // 构建可用目标列表
+ for (int j = 0; j < rawTargets.Count; j++)
+ {
+ if (rawTargets[j] != searcher && tmpCanShootAtTarget[j])
+ {
+ availableShootingTargets.Add(new Pair(rawTargets[j], tmpTargetScores[j]));
+ }
+ }
+
+ return availableShootingTargets;
+ }
+
+ ///
+ /// 计算射击目标分数(核心评分算法)
+ ///
+ private static float GetShootingTargetScore(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb, Vector3 angle)
+ {
+ float score = 60f; // 基础分数
+
+ // 距离因素:越近分数越高(最多40分)
+ float distance = (target.Thing.Position - searcher.Thing.Position).LengthHorizontal;
+ score -= Mathf.Min(distance, 40f);
+
+ // 目标正在瞄准自己:加分
+ if (target.TargetCurrentlyAimingAt == searcher.Thing)
+ score += 10f;
+
+ // 最近攻击目标:加分(如果最近攻击过这个目标)
+ if (searcher.LastAttackedTarget == target.Thing && Find.TickManager.TicksGame - searcher.LastAttackTargetTick <= 300)
+ score += 40f;
+
+ // 掩体因素:目标有掩体保护则减分
+ float blockChance = CoverUtility.CalculateOverallBlockChance(target.Thing.Position, searcher.Thing.Position, searcher.Thing.Map);
+ score -= blockChance * 10f;
+
+ // Pawn特定因素
+ if (target is Pawn pawnTarget)
+ {
+ // 非战斗人员减分
+ score -= NonCombatantScore(pawnTarget);
+
+ // 远程攻击目标特殊处理
+ if (verb.verbProps.ai_TargetHasRangedAttackScoreOffset != 0f &&
+ pawnTarget.CurrentEffectiveVerb != null &&
+ pawnTarget.CurrentEffectiveVerb.verbProps.Ranged)
+ {
+ score += verb.verbProps.ai_TargetHasRangedAttackScoreOffset;
+ }
+
+ // 倒地目标大幅减分
+ if (pawnTarget.Downed)
+ score -= 50f;
+ }
+
+ // 友军误伤因素
+ score += FriendlyFireBlastRadiusTargetScoreOffset(target, searcher, verb);
+ score += FriendlyFireConeTargetScoreOffset(target, searcher, verb);
+
+ // 角度因素:计算与理想角度的偏差
+ Vector3 targetDirection = (target.Thing.DrawPos - searcher.Thing.DrawPos).Yto0();
+ float angleDeviation = Vector3.Angle(angle, targetDirection);
+
+ // 防止除零错误
+ if (angleDeviation < 0.1f)
+ angleDeviation = 0.1f;
+
+ // 最终分数计算:考虑目标优先级因子和角度偏差
+ float finalScore = score * target.TargetPriorityFactor / angleDeviation;
+
+ // 确保返回正数
+ return Mathf.Max(finalScore, 0.01f);
+ }
+
+ ///
+ /// 计算非战斗人员分数
+ ///
+ private static float NonCombatantScore(Thing target)
+ {
+ if (!(target is Pawn pawn))
+ return 0f;
+
+ if (!pawn.IsCombatant())
+ return 50f; // 非战斗人员大幅减分
+
+ if (pawn.DevelopmentalStage.Juvenile())
+ return 25f; // 未成年人中等减分
+
+ return 0f; // 战斗成年人不减分
+ }
+
+ ///
+ /// 计算爆炸半径内的友军误伤分数偏移
+ ///
+ private static float FriendlyFireBlastRadiusTargetScoreOffset(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb)
+ {
+ // 检查是否启用了避免友军误伤半径
+ if (verb.verbProps.ai_AvoidFriendlyFireRadius <= 0f)
+ return 0f;
+
+ Map map = target.Thing.Map;
+ IntVec3 targetPosition = target.Thing.Position;
+ int cellCount = GenRadial.NumCellsInRadius(verb.verbProps.ai_AvoidFriendlyFireRadius);
+ float friendlyFireScore = 0f;
+
+ // 遍历爆炸半径内的所有单元格
+ for (int i = 0; i < cellCount; i++)
+ {
+ IntVec3 checkCell = targetPosition + GenRadial.RadialPattern[i];
+
+ if (!checkCell.InBounds(map))
+ continue;
+
+ bool hasLineOfSight = true;
+ List thingsInCell = checkCell.GetThingList(map);
+
+ // 检查单元格内的所有物体
+ for (int j = 0; j < thingsInCell.Count; j++)
+ {
+ Thing thing = thingsInCell[j];
+
+ // 只关心攻击目标且不是当前目标
+ if (!(thing is IAttackTarget) || thing == target)
+ continue;
+
+ // 检查视线(只检查一次)
+ if (hasLineOfSight)
+ {
+ if (!GenSight.LineOfSight(targetPosition, checkCell, map, true, null, 0, 0))
+ break; // 没有视线,跳过这个单元格
+
+ hasLineOfSight = false;
+ }
+
+ // 计算误伤分数
+ float hitScore;
+ if (thing == searcher)
+ hitScore = FriendlyFireScoreOffsetSelf; // 击中自己
+ else if (!(thing is Pawn))
+ hitScore = FriendlyFireScoreOffsetPerNonPawn; // 非Pawn物体
+ else if (thing.def.race.Animal)
+ hitScore = FriendlyFireScoreOffsetPerAnimal; // 动物
+ else
+ hitScore = FriendlyFireScoreOffsetPerHumanlikeOrMechanoid; // 人类或机械族
+
+ // 根据敌对关系调整分数
+ if (!searcher.Thing.HostileTo(thing))
+ friendlyFireScore -= hitScore; // 友军:减分
+ else
+ friendlyFireScore += hitScore * 0.6f; // 敌军:小幅加分
+ }
+ }
+
+ return friendlyFireScore;
+ }
+
+ ///
+ /// 计算锥形范围内的友军误伤分数偏移
+ ///
+ private static float FriendlyFireConeTargetScoreOffset(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb)
+ {
+ // 只对Pawn类型的搜索者进行计算
+ if (!(searcher.Thing is Pawn searcherPawn))
+ return 0f;
+
+ // 检查智能等级
+ if (searcherPawn.RaceProps.intelligence < Intelligence.ToolUser)
+ return 0f;
+
+ // 机械族不计算锥形误伤
+ if (searcherPawn.RaceProps.IsMechanoid)
+ return 0f;
+
+ // 只处理射击类动词
+ if (!(verb is Verb_Shoot shootVerb))
+ return 0f;
+
+ ThingDef projectileDef = shootVerb.verbProps.defaultProjectile;
+ if (projectileDef == null)
+ return 0f;
+
+ // 高空飞行的抛射物不计算锥形误伤
+ if (projectileDef.projectile.flyOverhead)
+ return 0f;
+
+ Map map = searcherPawn.Map;
+
+ // 获取射击报告
+ ShotReport report = ShotReport.HitReportFor(searcherPawn, verb, (Thing)target);
+
+ // 计算强制失误半径
+ float forcedMissRadius = Mathf.Max(
+ VerbUtility.CalculateAdjustedForcedMiss(verb.verbProps.ForcedMissRadius, report.ShootLine.Dest - report.ShootLine.Source),
+ 1.5f);
+
+ // 获取可能被误伤的所有单元格
+ IEnumerable potentialHitCells =
+ from dest in GenRadial.RadialCellsAround(report.ShootLine.Dest, forcedMissRadius, true)
+ where dest.InBounds(map)
+ select new ShootLine(report.ShootLine.Source, dest)
+ into line
+ from pos in line.Points().Concat(line.Dest).TakeWhile(pos => pos.CanBeSeenOverFast(map))
+ select pos;
+
+ potentialHitCells = potentialHitCells.Distinct();
+
+ float coneFriendlyFireScore = 0f;
+
+ // 计算锥形范围内的误伤分数
+ foreach (IntVec3 cell in potentialHitCells)
+ {
+ float interceptChance = VerbUtility.InterceptChanceFactorFromDistance(report.ShootLine.Source.ToVector3Shifted(), cell);
+
+ if (interceptChance <= 0f)
+ continue;
+
+ List thingsInCell = cell.GetThingList(map);
+
+ for (int i = 0; i < thingsInCell.Count; i++)
+ {
+ Thing thing = thingsInCell[i];
+
+ if (!(thing is IAttackTarget) || thing == target)
+ continue;
+
+ // 计算误伤分数
+ float hitScore;
+ if (thing == searcher)
+ hitScore = FriendlyFireScoreOffsetSelf;
+ else if (!(thing is Pawn))
+ hitScore = FriendlyFireScoreOffsetPerNonPawn;
+ else if (thing.def.race.Animal)
+ hitScore = FriendlyFireScoreOffsetPerAnimal;
+ else
+ hitScore = FriendlyFireScoreOffsetPerHumanlikeOrMechanoid;
+
+ // 根据拦截概率和敌对关系调整分数
+ hitScore *= interceptChance;
+ if (!searcher.Thing.HostileTo(thing))
+ hitScore = -hitScore; // 友军:减分
+ else
+ hitScore *= 0.6f; // 敌军:小幅加分
+
+ coneFriendlyFireScore += hitScore;
+ }
+ }
+
+ return coneFriendlyFireScore;
+ }
+ }
+}
diff --git a/Source/ArachnaeSwarm/Verbs/Verb_ShootWithOffset.cs b/Source/ArachnaeSwarm/Verbs/Verb_ShootWithOffset.cs
new file mode 100644
index 0000000..1543205
--- /dev/null
+++ b/Source/ArachnaeSwarm/Verbs/Verb_ShootWithOffset.cs
@@ -0,0 +1,262 @@
+using RimWorld;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using UnityEngine;
+using Verse;
+
+namespace ArachnaeSwarm
+{
+ public class PlaceWorker_ShowTurretWithOffsetRadius : PlaceWorker
+ {
+ public override AcceptanceReport AllowsPlacing(BuildableDef checkingDef, IntVec3 loc, Rot4 rot, Map map, Thing thingToIgnore = null, Thing thing = null)
+ {
+ VerbProperties verbProperties = ((ThingDef)checkingDef).building.turretGunDef.Verbs.Find((VerbProperties v) => v.verbClass == typeof(Verb_ShootWithOffset));
+ if (verbProperties.range > 0f)
+ {
+ GenDraw.DrawRadiusRing(loc, verbProperties.range);
+ }
+ if (verbProperties.minRange > 0f)
+ {
+ GenDraw.DrawRadiusRing(loc, verbProperties.minRange);
+ }
+ return true;
+ }
+ }
+ public class ModExtension_ShootWithOffset : DefModExtension
+ {
+ public Vector2 GetOffsetFor(int index)
+ {
+ Vector2 result;
+ if (this.offsets.NullOrEmpty())
+ {
+ result = Vector2.zero;
+ }
+ else
+ {
+ int index2 = index % this.offsets.Count;
+ result = this.offsets[index2];
+ }
+ return result;
+ }
+ public List offsets = new List();
+ }
+ public class Verb_ShootWithOffset : Verb_Shoot
+ {
+ public int offset = 0;
+ protected override bool TryCastShot()
+ {
+ bool num = BaseTryCastShot();
+ if (num && CasterIsPawn)
+ {
+ CasterPawn.records.Increment(RecordDefOf.ShotsFired);
+ }
+
+ return num;
+ }
+ protected bool BaseTryCastShot()
+ {
+
+ if (currentTarget.HasThing && currentTarget.Thing.Map != caster.Map)
+ {
+ return false;
+ }
+
+ ThingDef projectile = Projectile;
+ if (projectile == null)
+ {
+ return false;
+ }
+
+ ShootLine resultingLine;
+ bool flag = TryFindShootLineFromTo(caster.Position, currentTarget, out resultingLine);
+ if (verbProps.stopBurstWithoutLos && !flag)
+ {
+ return false;
+ }
+
+ if (base.EquipmentSource != null)
+ {
+ base.EquipmentSource.GetComp()?.Notify_ProjectileLaunched();
+ base.EquipmentSource.GetComp()?.UsedOnce();
+ }
+
+ lastShotTick = Find.TickManager.TicksGame;
+ Thing manningPawn = caster;
+ Thing equipmentSource = base.EquipmentSource;
+ CompMannable compMannable = caster.TryGetComp();
+ if (compMannable?.ManningPawn != null)
+ {
+ manningPawn = compMannable.ManningPawn;
+ equipmentSource = caster;
+ }
+
+ Vector3 drawPos = caster.DrawPos;
+ drawPos = ApplyProjectileOffset(drawPos, equipmentSource);
+ Projectile projectile2 = (Projectile)GenSpawn.Spawn(projectile, resultingLine.Source, caster.Map);
+ if (equipmentSource.TryGetComp(out CompUniqueWeapon comp))
+ {
+ foreach (WeaponTraitDef item in comp.TraitsListForReading)
+ {
+ if (item.damageDefOverride != null)
+ {
+ projectile2.damageDefOverride = item.damageDefOverride;
+ }
+
+ if (!item.extraDamages.NullOrEmpty())
+ {
+ Projectile projectile3 = projectile2;
+ if (projectile3.extraDamages == null)
+ {
+ projectile3.extraDamages = new List();
+ }
+
+ projectile2.extraDamages.AddRange(item.extraDamages);
+ }
+ }
+ }
+
+ if (verbProps.ForcedMissRadius > 0.5f)
+ {
+ float num = verbProps.ForcedMissRadius;
+ if (manningPawn is Pawn pawn)
+ {
+ num *= verbProps.GetForceMissFactorFor(equipmentSource, pawn);
+ }
+
+ float num2 = VerbUtility.CalculateAdjustedForcedMiss(num, currentTarget.Cell - caster.Position);
+ if (num2 > 0.5f)
+ {
+ IntVec3 forcedMissTarget = GetForcedMissTarget(num2);
+ if (forcedMissTarget != currentTarget.Cell)
+ {
+ ProjectileHitFlags projectileHitFlags = ProjectileHitFlags.NonTargetWorld;
+ if (Rand.Chance(0.5f))
+ {
+ projectileHitFlags = ProjectileHitFlags.All;
+ }
+
+ if (!canHitNonTargetPawnsNow)
+ {
+ projectileHitFlags &= ~ProjectileHitFlags.NonTargetPawns;
+ }
+
+ projectile2.Launch(manningPawn, drawPos, forcedMissTarget, currentTarget, projectileHitFlags, preventFriendlyFire, equipmentSource);
+ return true;
+ }
+ }
+ }
+
+ ShotReport shotReport = ShotReport.HitReportFor(caster, this, currentTarget);
+ Thing randomCoverToMissInto = shotReport.GetRandomCoverToMissInto();
+ ThingDef targetCoverDef = randomCoverToMissInto?.def;
+ if (verbProps.canGoWild && !Rand.Chance(shotReport.AimOnTargetChance_IgnoringPosture))
+ {
+ bool flyOverhead = projectile2?.def?.projectile != null && projectile2.def.projectile.flyOverhead;
+ resultingLine.ChangeDestToMissWild(shotReport.AimOnTargetChance_StandardTarget, flyOverhead, caster.Map);
+ ProjectileHitFlags projectileHitFlags2 = ProjectileHitFlags.NonTargetWorld;
+ if (Rand.Chance(0.5f) && canHitNonTargetPawnsNow)
+ {
+ projectileHitFlags2 |= ProjectileHitFlags.NonTargetPawns;
+ }
+
+ projectile2.Launch(manningPawn, drawPos, resultingLine.Dest, currentTarget, projectileHitFlags2, preventFriendlyFire, equipmentSource, targetCoverDef);
+ return true;
+ }
+
+ if (currentTarget.Thing != null && currentTarget.Thing.def.CanBenefitFromCover && !Rand.Chance(shotReport.PassCoverChance))
+ {
+ ProjectileHitFlags projectileHitFlags3 = ProjectileHitFlags.NonTargetWorld;
+ if (canHitNonTargetPawnsNow)
+ {
+ projectileHitFlags3 |= ProjectileHitFlags.NonTargetPawns;
+ }
+
+ projectile2.Launch(manningPawn, drawPos, randomCoverToMissInto, currentTarget, projectileHitFlags3, preventFriendlyFire, equipmentSource, targetCoverDef);
+ return true;
+ }
+
+ ProjectileHitFlags projectileHitFlags4 = ProjectileHitFlags.IntendedTarget;
+ if (canHitNonTargetPawnsNow)
+ {
+ projectileHitFlags4 |= ProjectileHitFlags.NonTargetPawns;
+ }
+
+ if (!currentTarget.HasThing || currentTarget.Thing.def.Fillage == FillCategory.Full)
+ {
+ projectileHitFlags4 |= ProjectileHitFlags.NonTargetWorld;
+ }
+ if (currentTarget.Thing != null)
+ {
+ projectile2.Launch(manningPawn, drawPos, currentTarget, currentTarget, projectileHitFlags4, preventFriendlyFire, equipmentSource, targetCoverDef);
+ }
+ else
+ {
+ projectile2.Launch(manningPawn, drawPos, resultingLine.Dest, currentTarget, projectileHitFlags4, preventFriendlyFire, equipmentSource, targetCoverDef);
+ }
+ return true;
+ }
+
+ private Vector3 ApplyProjectileOffset(Vector3 originalDrawPos, Thing equipmentSource)
+ {
+ if (equipmentSource != null)
+ {
+ // 获取投射物偏移的模组扩展
+ ModExtension_ShootWithOffset offsetExtension =
+ equipmentSource.def.GetModExtension();
+
+ if (offsetExtension != null && offsetExtension.offsets != null && offsetExtension.offsets.Count > 0)
+ {
+ // 获取当前连发射击的剩余次数
+ int burstShotsLeft = GetBurstShotsLeft();
+
+ // 计算从发射者到目标的角度
+ Vector3 targetPos = currentTarget.CenterVector3;
+ Vector3 casterPos = caster.DrawPos;
+ float rimworldAngle = targetPos.AngleToFlat(casterPos);
+
+ // 将RimWorld角度转换为适合偏移计算的角度
+ float correctedAngle = ConvertRimWorldAngleToOffsetAngle(rimworldAngle);
+
+ // 应用偏移并旋转到正确方向
+ Vector2 offset = offsetExtension.GetOffsetFor(burstShotsLeft);
+ Vector2 rotatedOffset = offset.RotatedBy(correctedAngle);
+
+ // 将2D偏移转换为3D并应用到绘制位置
+ originalDrawPos += new Vector3(rotatedOffset.x, 0f, rotatedOffset.y);
+ }
+ }
+
+ return originalDrawPos;
+ }
+
+ ///
+ /// 获取当前连发射击剩余次数
+ ///
+ /// 连发射击剩余次数
+ private int GetBurstShotsLeft()
+ {
+ if (burstShotsLeft >= 0)
+ {
+ return (int)burstShotsLeft;
+ }
+ return 0;
+ }
+
+ ///
+ /// 将RimWorld角度转换为偏移计算用的角度
+ /// RimWorld使用顺时针角度系统,需要转换为标准的数学角度系统
+ ///
+ /// RimWorld角度
+ /// 转换后的角度
+ private float ConvertRimWorldAngleToOffsetAngle(float rimworldAngle)
+ {
+ // RimWorld角度:0°=东,90°=北,180°=西,270°=南
+ // 转换为:0°=东,90°=南,180°=西,270°=北
+ return -rimworldAngle - 90f;
+ }
+
+ }
+}
\ No newline at end of file