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