using System.Collections.Generic; using RimWorld; using UnityEngine; using Verse; namespace WulaFallenEmpire { // Final, robust extension class for configuring path-based penetration. public class Wula_PathPierce_Extension : DefModExtension { // Set to a positive number for limited hits, or -1 for infinite penetration. public int maxHits = 3; // The percentage of damage lost per hit. 0.25 means 25% damage loss per hit. public float damageFalloff = 0.25f; // If true, this projectile will never cause friendly fire, regardless of game settings. public bool preventFriendlyFire = false; public FleckDef tailFleckDef; // 用于配置拖尾特效的 FleckDef public int fleckDelayTicks = 10; // 拖尾特效延迟生成时间(tick) } public class Projectile_WulaLineAttack : Bullet { private int hitCounter = 0; private List alreadyDamaged = new List(); private Vector3 lastTickPosition; private int Fleck_MakeFleckTick; // 拖尾特效的计时器 public int Fleck_MakeFleckTickMax = 1; // 拖尾特效的生成频率 public IntRange Fleck_MakeFleckNum = new IntRange(1, 1); // 每次生成的粒子数量 public FloatRange Fleck_Angle = new FloatRange(-180f, 180f); // 粒子角度 public FloatRange Fleck_Scale = new FloatRange(1f, 1f); // 粒子大小 public FloatRange Fleck_Speed = new FloatRange(0f, 0f); // 粒子速度 public FloatRange Fleck_Rotation = new FloatRange(-180f, 180f); // 粒子旋转 private Wula_PathPierce_Extension Props => def.GetModExtension(); public override void ExposeData() { base.ExposeData(); Scribe_Values.Look(ref hitCounter, "hitCounter", 0); Scribe_Collections.Look(ref alreadyDamaged, "alreadyDamaged", LookMode.Reference); Scribe_Values.Look(ref lastTickPosition, "lastTickPosition"); if (alreadyDamaged == null) { alreadyDamaged = new List(); } } public override void Launch(Thing launcher, Vector3 origin, LocalTargetInfo usedTarget, LocalTargetInfo intendedTarget, ProjectileHitFlags hitFlags, bool preventFriendlyFire = false, Thing equipment = null, ThingDef targetCoverDef = null) { base.Launch(launcher, origin, usedTarget, intendedTarget, hitFlags, preventFriendlyFire, equipment, targetCoverDef); this.lastTickPosition = origin; this.alreadyDamaged.Clear(); this.hitCounter = 0; // Friendly fire is prevented if EITHER the game setting is true OR the XML extension is true. this.preventFriendlyFire = preventFriendlyFire || (Props?.preventFriendlyFire ?? false); } protected override void Tick() { Vector3 startPos = this.lastTickPosition; base.Tick(); if (this.Destroyed) return; this.Fleck_MakeFleckTick++; // 只有当达到延迟时间后才开始生成Fleck if (this.Fleck_MakeFleckTick >= Props.fleckDelayTicks) { if (this.Fleck_MakeFleckTick >= (Props.fleckDelayTicks + this.Fleck_MakeFleckTickMax)) { this.Fleck_MakeFleckTick = Props.fleckDelayTicks; // 重置计时器,从延迟时间开始循环 } Map map = base.Map; int randomInRange = this.Fleck_MakeFleckNum.RandomInRange; Vector3 currentPosition = this.ExactPosition; // Current position of the bullet Vector3 previousPosition = this.lastTickPosition; // Previous position of the bullet for (int i = 0; i < randomInRange; i++) { float currentBulletAngle = ExactRotation.eulerAngles.y; // 使用子弹当前的水平旋转角度 float fleckRotationAngle = currentBulletAngle; // Fleck 的旋转角度与子弹方向一致 float velocityAngle = this.Fleck_Angle.RandomInRange + currentBulletAngle; // Fleck 的速度角度基于子弹方向加上随机偏移 float randomInRange2 = this.Fleck_Scale.RandomInRange; float randomInRange3 = this.Fleck_Speed.RandomInRange; if (Props?.tailFleckDef != null) { FleckCreationData dataStatic = FleckMaker.GetDataStatic(currentPosition, map, Props.tailFleckDef, randomInRange2); dataStatic.rotation = fleckRotationAngle; dataStatic.rotationRate = this.Fleck_Rotation.RandomInRange; dataStatic.velocityAngle = velocityAngle; dataStatic.velocitySpeed = randomInRange3; map.flecks.CreateFleck(dataStatic); } } } if (this.Destroyed) return; Vector3 endPos = this.ExactPosition; CheckPathForDamage(startPos, endPos); this.lastTickPosition = endPos; } protected override void Impact(Thing hitThing, bool blockedByShield = false) { CheckPathForDamage(lastTickPosition, this.ExactPosition); if (hitThing != null && alreadyDamaged.Contains(hitThing)) { base.Impact(null, blockedByShield); } else { base.Impact(hitThing, blockedByShield); } } private void CheckPathForDamage(Vector3 startPos, Vector3 endPos) { if (startPos == endPos) return; int maxHits = Props?.maxHits ?? 1; bool infinitePenetration = maxHits < 0; if (!infinitePenetration && hitCounter >= maxHits) return; Map map = this.Map; float distance = Vector3.Distance(startPos, endPos); Vector3 direction = (endPos - startPos).normalized; for (float i = 0; i < distance; i += 0.8f) { if (!infinitePenetration && hitCounter >= maxHits) break; Vector3 checkPos = startPos + direction * i; var thingsInCell = new HashSet(map.thingGrid.ThingsListAt(checkPos.ToIntVec3())); foreach (Thing thing in thingsInCell) { if (thing is Pawn pawn && pawn != this.launcher && !alreadyDamaged.Contains(pawn)) { bool shouldDamage = false; // Case 1: Always damage the intended target if it's a pawn. This allows hunting. if (this.intendedTarget.Thing == pawn) { shouldDamage = true; } // Case 2: Always damage hostile pawns in the path. else if (pawn.HostileTo(this.launcher)) { shouldDamage = true; } // Case 3: Damage non-hostiles (friendlies, neutrals) if the shot itself isn't marked to prevent friendly fire. else if (!this.preventFriendlyFire) { shouldDamage = true; } if (shouldDamage) { ApplyPathDamage(pawn); if (!infinitePenetration && hitCounter >= maxHits) break; } } } } } private void ApplyPathDamage(Pawn pawn) { Wula_PathPierce_Extension props = Props; float falloff = props?.damageFalloff ?? 0.25f; // Damage falloff now applies universally, even for infinite penetration. float damageMultiplier = Mathf.Pow(1f - falloff, hitCounter); int damageAmount = (int)(this.DamageAmount * damageMultiplier); if (damageAmount <= 0) return; var dinfo = new DamageInfo( this.def.projectile.damageDef, damageAmount, this.ArmorPenetration * damageMultiplier, this.ExactRotation.eulerAngles.y, this.launcher, null, this.equipmentDef, DamageInfo.SourceCategory.ThingOrUnknown, this.intendedTarget.Thing); pawn.TakeDamage(dinfo); alreadyDamaged.Add(pawn); hitCounter++; } } }