diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index 183e09f..b9a9454 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/Effects/ARA_Flecks.xml b/1.6/1.6/Defs/Effects/ARA_Flecks.xml new file mode 100644 index 0000000..7d862ce --- /dev/null +++ b/1.6/1.6/Defs/Effects/ARA_Flecks.xml @@ -0,0 +1,17 @@ + + + + + Fleck_Wula_Dark_Matter_Beam + MoteOverhead + 0.025 + 0.025 + 0.025 + + ArachnaeSwarm/Mote/ARA_Lighting_Beam_Horizon + (188, 112, 255, 180) + MoteGlow + + + + \ No newline at end of file diff --git a/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon_Laser.xml b/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon_Laser.xml index b13fbe2..a6ddcc4 100644 --- a/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon_Laser.xml +++ b/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon_Laser.xml @@ -44,7 +44,7 @@ true - 1 + 0 36 6 10 @@ -58,8 +58,7 @@ 0.32 Mote_Wula_Dark_Matter_Beam GraserBeam_End - 0.35 - + 0.6 0.6 @@ -67,12 +66,12 @@ - true + true - 4 - 30 + 6 + 18 0.7 Mote_Wula_Dark_Matter_Beam @@ -83,4 +82,89 @@ + + + WULA_RW_DM_AR_SuperArc + + 乌拉帝国一线部队所使用的由暗物质驱动的常规步枪的改造版。现在它发射的能量束会在命中后寻找并跳跃到附近的其他敌人身上,形成致命的能量链。 + Ultra + + Wula/Weapon/WULA_RW_DM_AR + Graphic_Single + 1.2 + + +
  • Wula_Ranged_Weapon_T4
  • +
    + 0.9 + Interact_ChargeRifle + + +
  • WULA_Cube_Productor_Energy
  • +
    + WULA_Synth_Weapon_4_DM_Base_Technology + UnfinishedWeapon +
    + + 400 + 200 + 4 + + + 40000 + 4.5 + 1 + 1 + 1 + 1 + 1.25 + + +
  • + ArachnaeSwarm.Verb_ShootBeamSplitAndChain + + + true + 0 + 36 + 6 + 10 + + + Wula_Dark_Matter_Beam + 90 + 0.5 + + + 0 + BeamGraser_Shooting + Fleck_BeamBurn + 0.32 + GraserBeam_End + Fleck_Wula_Dark_Matter_Beam + 0.5 + 1.5 + + + true + 3 + 7 + 0.8 + + Fleck_Wula_Dark_Matter_Beam + + + 3 + 12 + 0.6 + + Fleck_Wula_Dark_Matter_Beam +
  • +
    + None + +
  • RewardStandardQualitySuper
  • +
    +
    + \ No newline at end of file diff --git a/Content/Textures/ArachnaeSwarm/Mote/ARA_Lighting_Beam_Horizon.png b/Content/Textures/ArachnaeSwarm/Mote/ARA_Lighting_Beam_Horizon.png new file mode 100644 index 0000000..59120cd Binary files /dev/null and b/Content/Textures/ArachnaeSwarm/Mote/ARA_Lighting_Beam_Horizon.png differ diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj index a0be073..87a3515 100644 --- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj +++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj @@ -234,6 +234,7 @@ + @@ -265,6 +266,9 @@ + + + diff --git a/Source/ArachnaeSwarm/Utils/BezierUtil.cs b/Source/ArachnaeSwarm/Utils/BezierUtil.cs new file mode 100644 index 0000000..b3e4643 --- /dev/null +++ b/Source/ArachnaeSwarm/Utils/BezierUtil.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace ArachnaeSwarm.Utils +{ + public static class BezierUtil + { + // Generates points for a quadratic Bezier curve. + public static List GenerateQuadraticPoints(Vector3 start, Vector3 control, Vector3 end, int segments) + { + List points = new List(); + if (segments <= 0) segments = 1; + + for (int i = 0; i <= segments; i++) + { + float t = (float)i / segments; + float u = 1f - t; + float tt = t * t; + float uu = u * u; + + Vector3 p = uu * start; // (1-t)^2 * P0 + p += 2 * u * t * control; // 2(1-t)t * P1 + p += tt * end; // t^2 * P2 + + points.Add(p); + } + return points; + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Verbs/Verb_ShootBeamArc.cs b/Source/ArachnaeSwarm/Verbs/Verb_ShootBeamArc.cs index a0f5bc1..1cb2f6f 100644 --- a/Source/ArachnaeSwarm/Verbs/Verb_ShootBeamArc.cs +++ b/Source/ArachnaeSwarm/Verbs/Verb_ShootBeamArc.cs @@ -14,6 +14,7 @@ namespace ArachnaeSwarm public float conductRange; public float secondaryDamageFactor = 0.5f; public ThingDef chainMoteDef; + public float beamArmorPenetration = 0f; // Add missing property public VerbProperties_BeamArc() { @@ -21,70 +22,83 @@ namespace ArachnaeSwarm } } - // This class is a modified copy of Verb_ShootBeam to implement chain-lightning functionality. public class Verb_ShootBeamArc : Verb { - // --- Fields from original Verb_ShootBeam --- private int ticksToNextPathStep; - private MoteDualAttached mote; // This will be the main beam private Effecter endEffecter; private Sustainer sustainer; - // --- Custom fields for chain logic --- protected List chainedTargets = new List(); protected List chainMotes = new List(); private VerbProperties_BeamArc Props => this.verbProps as VerbProperties_BeamArc; protected override int ShotsPerBurst => base.BurstShotCount; - + public override void WarmupComplete() { - // --- Chain Target Finding Logic --- - chainedTargets.Clear(); - foreach (MoteDualAttached m in chainMotes) { m.Destroy(); } - chainMotes.Clear(); + this.Cleanup(); if (this.Props != null && this.Props.conductNum > 0 && this.currentTarget.HasThing) { - Thing currentTargetThing = this.currentTarget.Thing; - chainedTargets.Add(currentTargetThing); - - Thing lastTarget = currentTargetThing; - for (int i = 0; i < this.Props.conductNum; i++) + Thing primaryTarget = this.currentTarget.Thing; + if (primaryTarget is Pawn p && (p.Dead || p.Downed)) { - Thing nextTarget = AttackTargetFinder.BestAttackTarget(lastTarget as Pawn, TargetScanFlags.NeedLOSToAll, (Thing t) => - t is Pawn p && !p.Downed && !chainedTargets.Contains(t) && t.Position.InHorDistOf(lastTarget.Position, this.Props.conductRange) && this.Caster.HostileTo(t), - 0f, 9999f, default(IntVec3), this.Props.conductRange) as Thing; + // Do not start a chain on an invalid primary target. + } + else + { + chainedTargets.Add(primaryTarget); + Thing lastTarget = primaryTarget; - if (nextTarget != null) + for (int i = 0; i < this.Props.conductNum; i++) { - chainedTargets.Add(nextTarget); - lastTarget = nextTarget; + // MCP Suggested Fix: Manual search for the NEAREST valid target. + Thing nextTarget = GenRadial.RadialDistinctThingsAround(lastTarget.Position, this.caster.Map, this.Props.conductRange, false) + .OfType() + .Where(pawn => + !pawn.Dead && + !pawn.Downed && + !chainedTargets.Contains(pawn) && + this.Caster.HostileTo(pawn) && + GenSight.LineOfSight(lastTarget.Position, pawn.Position, this.caster.Map, true) + ) + .OrderBy(pawn => pawn.Position.DistanceToSquared(lastTarget.Position)) + .FirstOrDefault(); + + if (nextTarget != null) + { + chainedTargets.Add(nextTarget); + lastTarget = nextTarget; + } + else + { + break; + } } - else { break; } } } - // --- Original Verb_ShootBeam Logic (simplified) --- burstShotsLeft = ShotsPerBurst; state = VerbState.Bursting; - // Create main beam mote - if (verbProps.beamMoteDef != null && this.currentTarget.Thing != null) + // Unified visual creation + if (chainedTargets.Any()) { - mote = MoteMaker.MakeInteractionOverlay(verbProps.beamMoteDef, caster, this.currentTarget.Thing); - } - - // Create chain motes - if (chainedTargets.Count > 1) - { - for (int i = 0; i < chainedTargets.Count - 1; i++) + // First link: Caster -> Primary Target + if (verbProps.beamMoteDef != null) { - ThingDef moteDef = this.Props.chainMoteDef ?? this.verbProps.beamMoteDef; - if (moteDef != null) + MoteDualAttached firstLink = MoteMaker.MakeInteractionOverlay(verbProps.beamMoteDef, this.caster, chainedTargets[0]); + chainMotes.Add(firstLink); + } + + // Subsequent links: Target -> Next Target + if (chainedTargets.Count > 1) + { + ThingDef chainMoteDef = this.Props.chainMoteDef ?? this.verbProps.beamMoteDef; + for (int i = 0; i < chainedTargets.Count - 1; i++) { - MoteDualAttached chainLinkMote = MoteMaker.MakeInteractionOverlay(moteDef, chainedTargets[i], chainedTargets[i + 1]); - chainMotes.Add(chainLinkMote); + MoteDualAttached chainLink = MoteMaker.MakeInteractionOverlay(chainMoteDef, chainedTargets[i], chainedTargets[i + 1]); + chainMotes.Add(chainLink); } } } @@ -100,28 +114,26 @@ namespace ArachnaeSwarm public override void BurstingTick() { - // --- Update Visuals --- - mote?.Maintain(); + if (this.burstShotsLeft <= 0) + { + this.Cleanup(); + base.BurstingTick(); // Must be called to properly end the verb state. + return; + } + foreach (MoteDualAttached m in chainMotes) { m.Maintain(); } - // --- Original ground/end effect logic (simplified to target) --- - if (this.currentTarget.Thing != null) + // Simplified end effecter logic on the last known target. + Thing lastTarget = chainedTargets.LastOrDefault(); + if (lastTarget != null) { - Vector3 endPoint = this.currentTarget.Thing.DrawPos; - IntVec3 endCell = this.currentTarget.Cell; - - if (verbProps.beamGroundFleckDef != null && Rand.Chance(verbProps.beamFleckChancePerTick)) + if (endEffecter == null && verbProps.beamEndEffecterDef != null) { - FleckMaker.Static(endPoint, caster.Map, verbProps.beamGroundFleckDef); - } - if (endEffecter == null && verbProps.beamEndEffecterDef != null) - { - endEffecter = verbProps.beamEndEffecterDef.Spawn(endCell, caster.Map, Vector3.zero); + endEffecter = verbProps.beamEndEffecterDef.Spawn(lastTarget.Position, caster.Map, Vector3.zero); } if (endEffecter != null) { - endEffecter.EffectTick(new TargetInfo(endCell, caster.Map), TargetInfo.Invalid); - endEffecter.ticksLeft--; + endEffecter.EffectTick(new TargetInfo(lastTarget), TargetInfo.Invalid); } } sustainer?.Maintain(); @@ -129,7 +141,7 @@ namespace ArachnaeSwarm protected override bool TryCastShot() { - if (this.currentTarget.HasThing && this.currentTarget.Thing.Map != this.caster.Map) { return false; } + if (!this.chainedTargets.Any()) return false; if (base.EquipmentSource != null) { @@ -137,41 +149,44 @@ namespace ArachnaeSwarm base.EquipmentSource.GetComp()?.UsedOnce(); } - // --- Apply Damage to Chain --- - if (this.chainedTargets.Any()) + this.ApplyChainDamage(this.chainedTargets[0], 1.0f); + for (int i = 1; i < this.chainedTargets.Count; i++) { - this.ApplyChainDamage(this.chainedTargets[0], 1.0f); - for (int i = 1; i < this.chainedTargets.Count; i++) - { - this.ApplyChainDamage(this.chainedTargets[i], this.Props.secondaryDamageFactor); - } - } - else if(this.currentTarget.Thing != null) - { - this.ApplyChainDamage(this.currentTarget.Thing, 1.0f); + this.ApplyChainDamage(this.chainedTargets[i], this.Props.secondaryDamageFactor); } + this.ticksToNextPathStep = this.verbProps.ticksBetweenBurstShots; return true; } + private void Cleanup() + { + foreach (MoteDualAttached m in chainMotes) { m.Destroy(); } + chainMotes.Clear(); + endEffecter?.Cleanup(); + endEffecter = null; + sustainer?.End(); + sustainer = null; + chainedTargets.Clear(); + } + private void ApplyChainDamage(Thing thing, float damageFactor) { - Map map = this.caster.Map; if (thing == null || this.verbProps.beamDamageDef == null) { return; } - float angleFlat = (this.currentTarget.Cell - this.caster.Position).AngleFlat; + float angleFlat = (thing.Position - this.caster.Position).AngleFlat; BattleLogEntry_RangedImpact log = new BattleLogEntry_RangedImpact(this.caster, thing, this.currentTarget.Thing, base.EquipmentSource.def, null, null); DamageInfo dinfo; if (this.verbProps.beamTotalDamage > 0f) { float damagePerShot = this.verbProps.beamTotalDamage / (float)this.ShotsPerBurst; - dinfo = new DamageInfo(this.verbProps.beamDamageDef, damagePerShot * damageFactor, this.verbProps.beamDamageDef.defaultArmorPenetration, angleFlat, this.caster, null, base.EquipmentSource.def, DamageInfo.SourceCategory.ThingOrUnknown, this.currentTarget.Thing); + dinfo = new DamageInfo(this.verbProps.beamDamageDef, damagePerShot * damageFactor, this.Props.beamArmorPenetration, angleFlat, this.caster, null, base.EquipmentSource.def, DamageInfo.SourceCategory.ThingOrUnknown, this.currentTarget.Thing); } else { float amount = (float)this.verbProps.beamDamageDef.defaultDamage * damageFactor; - dinfo = new DamageInfo(this.verbProps.beamDamageDef, amount, this.verbProps.beamDamageDef.defaultArmorPenetration, angleFlat, this.caster, null, base.EquipmentSource.def, DamageInfo.SourceCategory.ThingOrUnknown, this.currentTarget.Thing); + dinfo = new DamageInfo(this.verbProps.beamDamageDef, amount, this.Props.beamArmorPenetration, angleFlat, this.caster, null, base.EquipmentSource.def, DamageInfo.SourceCategory.ThingOrUnknown, this.currentTarget.Thing); } thing.TakeDamage(dinfo).AssociateWithLog(log); @@ -186,7 +201,7 @@ namespace ArachnaeSwarm } else if (Rand.Chance(this.verbProps.beamChanceToStartFire)) { - FireUtility.TryStartFireIn(thing.Position, map, this.verbProps.beamFireSizeRange.RandomInRange, this.caster, this.verbProps.flammabilityAttachFireChanceCurve); + FireUtility.TryStartFireIn(thing.Position, this.caster.Map, this.verbProps.beamFireSizeRange.RandomInRange, this.caster, this.verbProps.flammabilityAttachFireChanceCurve); } } } diff --git a/Source/ArachnaeSwarm/Verbs/Verb_ShootBeamSplitAndChain.cs b/Source/ArachnaeSwarm/Verbs/Verb_ShootBeamSplitAndChain.cs new file mode 100644 index 0000000..8565011 --- /dev/null +++ b/Source/ArachnaeSwarm/Verbs/Verb_ShootBeamSplitAndChain.cs @@ -0,0 +1,218 @@ +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.AI; +using Verse.Sound; +using ArachnaeSwarm.Utils; + +namespace ArachnaeSwarm +{ + public class VerbProperties_SplitAndChain : VerbProperties + { + public bool isSplit = false; + public int splitNum; + public float splitRange; + public int conductNum; + public float conductRange; + public float splitDamageFactor = 0.8f; + public float conductDamageFactor = 0.6f; + public float beamArmorPenetration = 0f; + public int beamPathSteps = 15; + public float flecksPerCell = 2f; // Flecks per cell to control beam density + + public FleckDef splitMoteDef; + public FleckDef chainMoteDef; + + public VerbProperties_SplitAndChain() + { + this.verbClass = typeof(Verb_ShootBeamSplitAndChain); + } + } + + public class Verb_ShootBeamSplitAndChain : Verb + { + private VerbProperties_SplitAndChain Props => this.verbProps as VerbProperties_SplitAndChain; + private Dictionary> attackChains = new Dictionary>(); + private Dictionary endEffecters = new Dictionary(); + private Sustainer sustainer; + private int ticksToNextPathStep; + + public override void WarmupComplete() + { + this.Cleanup(); + + List mainTargets = new List(); + if (!this.currentTarget.HasThing) { base.WarmupComplete(); return; } + + Thing primaryTarget = this.currentTarget.Thing; + if (primaryTarget is Pawn p_primary && (p_primary.Dead || p_primary.Downed)) return; + + mainTargets.Add(primaryTarget); + + if (this.Props.isSplit && this.Props.splitNum > 0) + { + var potentialTargets = GenRadial.RadialDistinctThingsAround(primaryTarget.Position, this.caster.Map, this.Props.splitRange, false) + .OfType() + .Where(p => !p.Dead && !p.Downed && p.HostileTo(this.caster.Faction) && !mainTargets.Contains(p) && GenSight.LineOfSight(primaryTarget.Position, p.Position, this.caster.Map, true)) + .OrderBy(p => p.Position.DistanceToSquared(primaryTarget.Position)) + .Take(this.Props.splitNum); + + mainTargets.AddRange(potentialTargets); + } + + foreach (Thing mainTarget in mainTargets) + { + List currentChain = new List(); + currentChain.Add(mainTarget); + + Thing lastTargetInChain = mainTarget; + for (int i = 0; i < this.Props.conductNum; i++) + { + Thing nextInChain = GenRadial.RadialDistinctThingsAround(lastTargetInChain.Position, this.caster.Map, this.Props.conductRange, false) + .OfType() + .Where(p => !p.Dead && !p.Downed && !currentChain.Contains(p) && !mainTargets.Except(new[]{mainTarget}).Contains(p) && this.Caster.HostileTo(p) && GenSight.LineOfSight(lastTargetInChain.Position, p.Position, this.caster.Map, true)) + .OrderBy(p => p.Position.DistanceToSquared(lastTargetInChain.Position)) + .FirstOrDefault(); + + if (nextInChain != null) + { + currentChain.Add(nextInChain); + lastTargetInChain = nextInChain; + } + else { break; } + } + attackChains[mainTarget] = currentChain; + } + + this.burstShotsLeft = this.verbProps.burstShotCount; + this.state = VerbState.Bursting; + if (this.Props.soundCastBeam != null) + { + this.sustainer = this.Props.soundCastBeam.TrySpawnSustainer(SoundInfo.InMap(this.caster, MaintenanceType.PerTick)); + } + base.TryCastNextBurstShot(); + } + + public override void BurstingTick() + { + if (this.burstShotsLeft <= 0) + { + this.Cleanup(); + base.BurstingTick(); + return; + } + + List deadOrInvalidChains = attackChains.Keys.Where(t => t == null || !t.Spawned).ToList(); + foreach (var key in deadOrInvalidChains) + { + if(endEffecters.ContainsKey(key)) + { + endEffecters[key].Cleanup(); + endEffecters.Remove(key); + } + attackChains.Remove(key); + } + + Vector3 casterPos = this.caster.DrawPos; + foreach (var chainEntry in attackChains) + { + Thing mainTarget = chainEntry.Key; + List conductTargets = chainEntry.Value; + + DrawCurvedBeam(casterPos, mainTarget.DrawPos, Props.splitMoteDef ?? verbProps.beamLineFleckDef); + + for (int i = 0; i < conductTargets.Count - 1; i++) + { + DrawCurvedBeam(conductTargets[i].DrawPos, conductTargets[i+1].DrawPos, Props.chainMoteDef ?? verbProps.beamLineFleckDef); + } + + foreach (Thing target in conductTargets) + { + if (!endEffecters.ContainsKey(target) || endEffecters[target] == null) + { + endEffecters[target] = verbProps.beamEndEffecterDef?.Spawn(target.Position, target.Map, Vector3.zero); + } + endEffecters[target]?.EffectTick(new TargetInfo(target), TargetInfo.Invalid); + } + } + sustainer?.Maintain(); + } + + protected override bool TryCastShot() + { + if (this.attackChains.NullOrEmpty()) return false; + + bool anyDamaged = false; + foreach (var chainEntry in attackChains) + { + Thing mainTarget = chainEntry.Key; + List conductTargets = chainEntry.Value; + + ApplyDamage(mainTarget, Props.splitDamageFactor); + anyDamaged = true; + + for (int i = 1; i < conductTargets.Count; i++) + { + ApplyDamage(conductTargets[i], Props.conductDamageFactor); + } + } + + this.ticksToNextPathStep = this.verbProps.ticksBetweenBurstShots; + return anyDamaged; + } + + private void DrawCurvedBeam(Vector3 start, Vector3 end, FleckDef fleckDef) + { + if (fleckDef == null) return; + + float magnitude = (end - start).MagnitudeHorizontal(); + if (magnitude <= 0) return; + + // 1. Generate Bezier curve points + int segments = Mathf.Max(3, Mathf.CeilToInt(magnitude * Props.flecksPerCell)); + + Vector3 controlPoint = Vector3.Lerp(start, end, 0.5f) + new Vector3(0, -magnitude * Props.beamCurvature, 0); + var path = BezierUtil.GenerateQuadraticPoints(start, controlPoint, end, segments); + // 2. Check if there are enough points to connect + if (path.Count < 2) + { + return; + } + + // 3. Iterate through adjacent point pairs and draw connecting lines + for (int i = 0; i < path.Count - 1; i++) + { + Vector3 pointA = path[i]; + Vector3 pointB = path[i + 1]; + FleckMaker.ConnectingLine(pointA, pointB, fleckDef, this.caster.Map, 1f); + } + } + + private void ApplyDamage(Thing thing, float damageFactor) + { + if (thing == null || verbProps.beamDamageDef == null) return; + + float totalDamage = verbProps.beamTotalDamage > 0 ? verbProps.beamTotalDamage / verbProps.burstShotCount : verbProps.beamDamageDef.defaultDamage; + float finalDamage = totalDamage * damageFactor; + + var dinfo = new DamageInfo(verbProps.beamDamageDef, finalDamage, Props.beamArmorPenetration, -1, this.caster, null, base.EquipmentSource.def); + thing.TakeDamage(dinfo); + } + + private void Cleanup() + { + attackChains.Clear(); + foreach (var effecter in endEffecters.Values) effecter.Cleanup(); + endEffecters.Clear(); + sustainer?.End(); + sustainer = null; + } + + public override void ExposeData() + { + base.ExposeData(); + } + } +} \ No newline at end of file