diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index 9b22b4c..81bed62 100644 Binary files a/1.6/1.6/Assemblies/ArachnaeSwarm.dll and b/1.6/1.6/Assemblies/ArachnaeSwarm.dll differ diff --git a/Source/ArachnaeSwarm/.vs/ArachnaeSwarm/v17/.suo b/Source/ArachnaeSwarm/.vs/ArachnaeSwarm/v17/.suo index 1b0ba5b..047ac6f 100644 Binary files a/Source/ArachnaeSwarm/.vs/ArachnaeSwarm/v17/.suo and b/Source/ArachnaeSwarm/.vs/ArachnaeSwarm/v17/.suo differ diff --git a/Source/ArachnaeSwarm/.vs/ArachnaeSwarm/v17/DocumentLayout.json b/Source/ArachnaeSwarm/.vs/ArachnaeSwarm/v17/DocumentLayout.json index 5222678..dbfd2c0 100644 --- a/Source/ArachnaeSwarm/.vs/ArachnaeSwarm/v17/DocumentLayout.json +++ b/Source/ArachnaeSwarm/.vs/ArachnaeSwarm/v17/DocumentLayout.json @@ -3,8 +3,12 @@ "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\\ara_hivemind\\compabilityeffect_binddrone.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|solutionrelative:ara_hivemind\\compabilityeffect_binddrone.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\\verb\\verb_shootmeltbeam.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|solutionrelative:verb\\verb_shootmeltbeam.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\\verb\\compcleave.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|solutionrelative:verb\\compcleave.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" } ], "DocumentGroupContainers": [ @@ -19,19 +23,32 @@ { "$type": "Document", "DocumentIndex": 0, - "Title": "CompAbilityEffect_BindDrone.cs", - "DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\ARA_HiveMind\\CompAbilityEffect_BindDrone.cs", - "RelativeDocumentMoniker": "ARA_HiveMind\\CompAbilityEffect_BindDrone.cs", - "ToolTip": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\ARA_HiveMind\\CompAbilityEffect_BindDrone.cs", - "RelativeToolTip": "ARA_HiveMind\\CompAbilityEffect_BindDrone.cs", - "ViewState": "AgIAAAMAAAAAAAAAAAAAABEAAABkAAAAAAAAAA==", + "Title": "Verb_ShootMeltBeam.cs", + "DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Verb\\Verb_ShootMeltBeam.cs", + "RelativeDocumentMoniker": "Verb\\Verb_ShootMeltBeam.cs", + "ToolTip": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Verb\\Verb_ShootMeltBeam.cs", + "RelativeToolTip": "Verb\\Verb_ShootMeltBeam.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAA8AAAAyAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2025-09-09T02:52:35.777Z", + "WhenOpened": "2025-09-09T03:04:00.299Z", "EditorCaption": "" }, { "$type": "Bookmark", "Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}" + }, + { + "$type": "Document", + "DocumentIndex": 1, + "Title": "CompCleave.cs", + "DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Verb\\CompCleave.cs", + "RelativeDocumentMoniker": "Verb\\CompCleave.cs", + "ToolTip": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Verb\\CompCleave.cs", + "RelativeToolTip": "Verb\\CompCleave.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAwAAAAmAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-09-09T03:03:30.118Z", + "EditorCaption": "" } ] } diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj index 7ea45a9..713ad63 100644 --- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj +++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj @@ -90,6 +90,18 @@ + + + + + + + + + + + + diff --git a/Source/ArachnaeSwarm/Verb/CompCleave.cs b/Source/ArachnaeSwarm/Verb/CompCleave.cs new file mode 100644 index 0000000..e969134 --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/CompCleave.cs @@ -0,0 +1,23 @@ +using Verse; + +namespace ArachnaeSwarm +{ + public class CompProperties_Cleave : CompProperties + { + public float cleaveAngle = 90f; + public float cleaveRange = 2.9f; + public float cleaveDamageFactor = 0.7f; + public bool damageDowned = false; + public DamageDef explosionDamageDef = null; + + public CompProperties_Cleave() + { + this.compClass = typeof(CompCleave); + } + } + + public class CompCleave : ThingComp + { + public CompProperties_Cleave Props => (CompProperties_Cleave)this.props; + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Verb/CompMultiStrike.cs b/Source/ArachnaeSwarm/Verb/CompMultiStrike.cs new file mode 100644 index 0000000..c615eb0 --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/CompMultiStrike.cs @@ -0,0 +1,20 @@ +using Verse; + +namespace ArachnaeSwarm +{ + public class CompProperties_MultiStrike : CompProperties + { + public IntRange strikeCount = new IntRange(3, 3); + public float damageMultiplierPerStrike = 0.5f; + + public CompProperties_MultiStrike() + { + compClass = typeof(CompMultiStrike); + } + } + + public class CompMultiStrike : ThingComp + { + public CompProperties_MultiStrike Props => (CompProperties_MultiStrike)props; + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Verb/VerbPropertiesExplosiveBeam.cs b/Source/ArachnaeSwarm/Verb/VerbPropertiesExplosiveBeam.cs new file mode 100644 index 0000000..1288fb3 --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/VerbPropertiesExplosiveBeam.cs @@ -0,0 +1,50 @@ +using RimWorld; +using Verse; +using Verse.Sound; + +namespace ArachnaeSwarm +{ + public class VerbPropertiesExplosiveBeam : VerbProperties + { + // 爆炸开关 + public bool enableExplosion = false; + + // 每x个shotcount触发一次爆炸 + public int explosionShotInterval = 1; + + // 爆炸基础属性 + public float explosionRadius = 2.9f; + public DamageDef explosionDamageDef = null; // null时使用默认的Bomb + public int explosionDamage = -1; // -1时使用武器默认伤害 + public float explosionArmorPenetration = -1f; // -1时使用武器默认穿甲 + + // 爆炸音效和特效 + public SoundDef explosionSound = null; + public EffecterDef explosionEffecter = null; + + // 爆炸后生成物品 + public ThingDef postExplosionSpawnThingDef = null; + public float postExplosionSpawnChance = 0f; + public int postExplosionSpawnThingCount = 1; + + // 爆炸前生成物品 + public ThingDef preExplosionSpawnThingDef = null; + public float preExplosionSpawnChance = 0f; + public int preExplosionSpawnThingCount = 1; + + // 气体效果 + public GasType? postExplosionGasType = null; + + // 其他爆炸属性 + public bool applyDamageToExplosionCellsNeighbors = true; + public float chanceToStartFire = 0f; + public bool damageFalloff = true; + public float screenShakeFactor = 0f; // 新增:屏幕震动因子 + + public VerbPropertiesExplosiveBeam() + { + // 设置默认值 + verbClass = typeof(Verb_ShootBeamExplosive); + } + } +} diff --git a/Source/ArachnaeSwarm/Verb/VerbProperties_Excalibur.cs b/Source/ArachnaeSwarm/Verb/VerbProperties_Excalibur.cs new file mode 100644 index 0000000..c4595bb --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/VerbProperties_Excalibur.cs @@ -0,0 +1,15 @@ +using Verse; +using RimWorld; + +namespace ArachnaeSwarm +{ + public class VerbProperties_Excalibur : VerbProperties + { + public float pathWidth = 1f; // Default path width + public DamageDef damageDef; // Custom damage type + public float damageAmount = -1f; // Custom damage amount + public float armorPenetration = -1f; // Custom armor penetration + public float maxRange = 1000f; // Default max range for beams + public string beamDefName = "ExcaliburBeam"; // Default beam def name + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Verb/VerbProperties_WeaponStealBeam.cs b/Source/ArachnaeSwarm/Verb/VerbProperties_WeaponStealBeam.cs new file mode 100644 index 0000000..dfba763 --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/VerbProperties_WeaponStealBeam.cs @@ -0,0 +1,18 @@ +using RimWorld; +using Verse; + +namespace ArachnaeSwarm +{ + public class VerbProperties_WeaponStealBeam : VerbPropertiesExplosiveBeam + { + public HediffDef hediffToApply; + public float hediffSeverityPerHit = 0.1f; // 每次命中增加的严重性百分比 + public float hediffMaxSeverity = 1.0f; // 达到此严重性时触发抢夺 + public bool removeHediffOnSteal = true; // 抢夺后是否移除hediff + + public VerbProperties_WeaponStealBeam() + { + verbClass = typeof(Verb_ShootWeaponStealBeam); + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Verb/Verb_MeleeAttack_Cleave.cs b/Source/ArachnaeSwarm/Verb/Verb_MeleeAttack_Cleave.cs new file mode 100644 index 0000000..a4a7bfa --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/Verb_MeleeAttack_Cleave.cs @@ -0,0 +1,180 @@ +using RimWorld; +using Verse; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace ArachnaeSwarm +{ + public class Verb_MeleeAttack_Cleave : Verb_MeleeAttack + { + private CompCleave Comp + { + get + { + return this.EquipmentSource?.GetComp(); + } + } + + protected override DamageWorker.DamageResult ApplyMeleeDamageToTarget(LocalTargetInfo target) + { + if (this.Comp == null) + { + // This verb should only be used with a weapon that has CompCleave + return new DamageWorker.DamageResult(); + } + + DamageWorker.DamageResult result = new DamageWorker.DamageResult(); + + // 1. 对主目标造成伤害 + DamageInfo dinfo = new DamageInfo( + this.verbProps.meleeDamageDef, + this.verbProps.AdjustedMeleeDamageAmount(this, this.CasterPawn), + this.verbProps.AdjustedArmorPenetration(this, this.CasterPawn), + -1f, + this.CasterPawn, + null, + this.EquipmentSource?.def + ); + dinfo.SetTool(this.tool); + + if (target.HasThing) + { + result = target.Thing.TakeDamage(dinfo); + } + + // 2. 执行溅射伤害 + Pawn casterPawn = this.CasterPawn; + if (casterPawn == null || !target.HasThing) + { + return result; + } + + Thing mainTarget = target.Thing; + Vector3 attackDirection = (mainTarget.Position - casterPawn.Position).ToVector3().normalized; + bool mainTargetIsHostile = mainTarget.HostileTo(casterPawn); + + // 查找施法者周围的潜在目标 + IEnumerable potentialTargets = GenRadial.RadialDistinctThingsAround(casterPawn.Position, casterPawn.Map, this.Comp.Props.cleaveRange, useCenter: true); + + foreach (Thing thing in potentialTargets) + { + // 跳过主目标、自己和非生物 + if (thing == mainTarget || thing == casterPawn || !(thing is Pawn secondaryTargetPawn)) + { + continue; + } + + // 根据XML配置决定是否跳过倒地的生物 + if (!this.Comp.Props.damageDowned && secondaryTargetPawn.Downed) + { + continue; + } + + // 智能溅射:次要目标的敌对状态必须与主目标一致 + if (secondaryTargetPawn.HostileTo(casterPawn) != mainTargetIsHostile) + { + continue; + } + + // 检查目标是否在攻击扇形范围内 + Vector3 directionToTarget = (thing.Position - casterPawn.Position).ToVector3().normalized; + float angle = Vector3.Angle(attackDirection, directionToTarget); + + if (angle <= this.Comp.Props.cleaveAngle / 2f) + { + // 对次要目标造成伤害 + DamageInfo cleaveDinfo = new DamageInfo( + this.verbProps.meleeDamageDef, + this.verbProps.AdjustedMeleeDamageAmount(this, casterPawn) * this.Comp.Props.cleaveDamageFactor, + this.verbProps.AdjustedArmorPenetration(this, casterPawn) * this.Comp.Props.cleaveDamageFactor, + -1f, + casterPawn, + null, + this.EquipmentSource?.def + ); + cleaveDinfo.SetTool(this.tool); + secondaryTargetPawn.TakeDamage(cleaveDinfo); + } + } + + // 3. 创建扇形爆炸效果 + CreateCleaveExplosion(casterPawn, mainTarget, this.Comp.Props.cleaveRange, this.Comp.Props.cleaveAngle); + + return result; + } + + private void CreateCleaveExplosion(Pawn caster, Thing target, float radius, float angle) + { + if (caster.Map == null || this.Comp.Props.explosionDamageDef == null) return; + + Vector3 direction = (target.Position - caster.Position).ToVector3().normalized; + float baseAngle = direction.AngleFlat(); + + float startAngle = baseAngle - (angle / 2f); + float endAngle = baseAngle + (angle / 2f); + + GenExplosion.DoExplosion( + center: caster.Position, + map: caster.Map, + radius: radius, + damType: this.Comp.Props.explosionDamageDef, + instigator: caster, + damAmount: 0, + armorPenetration: 0, + explosionSound: null, + weapon: this.EquipmentSource?.def, + projectile: null, + intendedTarget: target, + postExplosionSpawnThingDef: null, + postExplosionSpawnChance: 0f, + postExplosionSpawnThingCount: 1, + postExplosionGasType: null, + applyDamageToExplosionCellsNeighbors: false, + preExplosionSpawnThingDef: null, + preExplosionSpawnChance: 0f, + preExplosionSpawnThingCount: 1, + chanceToStartFire: 0f, + damageFalloff: false, + direction: null, // Let affectedAngle handle the direction and arc + ignoredThings: null, + affectedAngle: new FloatRange(startAngle, endAngle), + doVisualEffects: true, + propagationSpeed: 1.7f, + excludeRadius: 0.9f, + doSoundEffects: false, + screenShakeFactor: 0.2f + ); + } + + public override void DrawHighlight(LocalTargetInfo target) + { + base.DrawHighlight(target); + + if (target.IsValid && CasterPawn != null && this.Comp != null) + { + GenDraw.DrawFieldEdges(GetCleaveCells(target.Cell)); + } + } + + private List GetCleaveCells(IntVec3 center) + { + if (this.Comp == null) + { + return new List(); + } + + IntVec3 casterPos = this.CasterPawn.Position; + Map map = this.CasterPawn.Map; + Vector3 attackDirection = (center - casterPos).ToVector3().normalized; + + return GenRadial.RadialCellsAround(casterPos, this.Comp.Props.cleaveRange, useCenter: true) + .Where(cell => { + if (!cell.InBounds(map)) return false; + Vector3 directionToCell = (cell - casterPos).ToVector3(); + if (directionToCell.sqrMagnitude <= 0.001f) return false; // Exclude caster's cell + return Vector3.Angle(attackDirection, directionToCell) <= this.Comp.Props.cleaveAngle / 2f; + }).ToList(); + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Verb/Verb_MeleeAttack_MultiStrike.cs b/Source/ArachnaeSwarm/Verb/Verb_MeleeAttack_MultiStrike.cs new file mode 100644 index 0000000..ca731f6 --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/Verb_MeleeAttack_MultiStrike.cs @@ -0,0 +1,53 @@ +using RimWorld; +using Verse; + +namespace ArachnaeSwarm +{ + public class Verb_MeleeAttack_MultiStrike : Verb_MeleeAttack + { + private CompMultiStrike Comp + { + get + { + return this.EquipmentSource?.GetComp(); + } + } + + protected override DamageWorker.DamageResult ApplyMeleeDamageToTarget(LocalTargetInfo target) + { + DamageWorker.DamageResult result = new DamageWorker.DamageResult(); + if (this.Comp != null && target.HasThing) + { + int strikes = this.Comp.Props.strikeCount.RandomInRange; + for (int i = 0; i < strikes; i++) + { + if (target.ThingDestroyed) + { + break; + } + DamageInfo dinfo = new DamageInfo(this.verbProps.meleeDamageDef, this.verbProps.AdjustedMeleeDamageAmount(this, this.CasterPawn) * this.Comp.Props.damageMultiplierPerStrike, this.verbProps.AdjustedArmorPenetration(this, this.CasterPawn) * this.Comp.Props.damageMultiplierPerStrike, -1f, this.CasterPawn, null, this.EquipmentSource?.def); + dinfo.SetTool(this.tool); + DamageWorker.DamageResult damageResult = target.Thing.TakeDamage(dinfo); + result.totalDamageDealt += damageResult.totalDamageDealt; + result.wounded = (result.wounded || damageResult.wounded); + result.stunned = (result.stunned || damageResult.stunned); + if (damageResult.parts != null) + { + if (result.parts == null) + { + result.parts = new System.Collections.Generic.List(); + } + result.parts.AddRange(damageResult.parts); + } + } + } + else + { + DamageInfo dinfo2 = new DamageInfo(this.verbProps.meleeDamageDef, this.verbProps.AdjustedMeleeDamageAmount(this, this.CasterPawn), this.verbProps.AdjustedArmorPenetration(this, this.CasterPawn), -1f, this.CasterPawn, null, this.EquipmentSource?.def); + dinfo2.SetTool(this.tool); + result = target.Thing.TakeDamage(dinfo2); + } + return result; + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Verb/Verb_ShootArc.cs b/Source/ArachnaeSwarm/Verb/Verb_ShootArc.cs new file mode 100644 index 0000000..cc3d2bb --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/Verb_ShootArc.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace ArachnaeSwarm +{ + public class VerbProperties_Arc : VerbProperties + { + public DamageDef damageDef; + + public float EMPDamageAmount = -1f; + + public int damageAmount = -1; + + public float armorPenetration = -1f; + + public float affectedAngle; + + public bool isConductible = false; + + public int conductNum; + + public bool conductFriendly = false; + + public bool conductHostile = true; + } + + public class Verb_ShootArc : Verb + { + private VerbProperties_Arc Props + { + get + { + return (VerbProperties_Arc)this.verbProps; + } + } + + private int damageAmount + { + get + { + if (this.Props.damageAmount > 0) + { + return this.Props.damageAmount; + } + if (this.verbProps.beamDamageDef != null) + { + return this.verbProps.beamDamageDef.defaultDamage; + } + Log.ErrorOnce(string.Format("Verb_ShootArc on {0} has no damageAmount and no beamDamageDef.", (this.caster != null) ? this.caster.def.defName : "null"), this.GetHashCode()); + return 0; + } + } + + private float armorPenetration + { + get + { + if (this.Props.armorPenetration > 0f) + { + return this.Props.armorPenetration; + } + if (this.verbProps.beamDamageDef != null) + { + return this.verbProps.beamDamageDef.defaultArmorPenetration; + } + return 0f; + } + } + + public override void WarmupComplete() + { + this.TryCastShot(); + } + + protected override bool TryCastShot() + { + this.MakeExplosion(); + bool flag = this.verbProps.soundCast != null; + bool flag3 = flag; + if (flag3) + { + this.verbProps.soundCast.PlayOneShot(new TargetInfo(this.caster.Position, this.caster.MapHeld, false)); + } + bool flag2 = this.verbProps.soundCastTail != null; + bool flag4 = flag2; + if (flag4) + { + this.verbProps.soundCastTail.PlayOneShotOnCamera(this.caster.Map); + } + return true; + } + + private bool IsTargetImmobile(LocalTargetInfo target) + { + Thing thing = target.Thing; + Pawn pawn = thing as Pawn; + return pawn != null && !pawn.Downed && pawn.GetPosture() == PawnPosture.Standing; + } + + public override bool CanHitTarget(LocalTargetInfo targ) + { + bool flag = this.caster == null || !this.caster.Spawned; + bool flag2 = flag; + return !flag2 && (targ == this.caster || this.CanHitTargetFrom(this.caster.Position, targ)); + } + + protected void MakeExplosion() + { + Pawn casterPawn = this.CasterPawn; + if (!casterPawn.Spawned || this.Props == null) + { + return; + } + + // 技能学习逻辑 (只在目标是站立Pawn时) + if (this.currentTarget.Thing is Pawn targetPawn && !targetPawn.Downed && targetPawn.GetPosture() == PawnPosture.Standing && casterPawn.skills != null) + { + casterPawn.skills.Learn(SkillDefOf.Shooting, 250f * verbProps.AdjustedFullCycleTime(this, casterPawn), false, false); + } + + float weaponDamageMultiplier = base.EquipmentSource.GetStatValue(StatDefOf.RangedWeapon_DamageMultiplier, true, -1); + int damageMultiplier = this.GetDamageAmount(weaponDamageMultiplier, null); + float armorPenetrationMultiplier = this.GetArmorPenetration(weaponDamageMultiplier, null); + + // 总是先收集范围内的Pawn,为后续决策做准备 + List cells = Verb_ShootArc.circularSectorCellsStartedCaster(casterPawn.Position, casterPawn.Map, this.currentTarget.Cell, this.Props.range, this.Props.affectedAngle, false).ToList(); + HashSet hashSet = this.HashSetConverter(cells); + this.pawnConduct.Add(casterPawn); + + foreach (IntVec3 cell in hashSet) + { + List list = casterPawn.Map.thingGrid.ThingsListAt(cell); + for (int num = list.Count - 1; num >= 0; num--) + { + if (list[num] is Pawn p) + { + bool isFriendly = p.Faction != null && casterPawn.Faction != null && !p.Faction.HostileTo(casterPawn.Faction); + if ((!this.Props.conductFriendly && isFriendly) || (!this.Props.conductHostile && p.HostileTo(casterPawn))) + { + continue; + } + bool isInvalidPosture = p.GetPosture() != PawnPosture.Standing && this.currentTarget.Thing != p; + if (!isInvalidPosture) + { + this.pawnConduct.Add(p); + } + } + } + } + + // 决策:如果设为导电模式且有至少一个传导目标,则进行链式攻击 + if (this.Props.isConductible && this.pawnConduct.Count > 1) + { + for (int i = 0; i < this.Props.conductNum && i < this.pawnConduct.Count - 1; i++) + { + if (this.Props.EMPDamageAmount > 0f) + { + this.TargetTakeDamage(casterPawn, this.pawnConduct[i + 1], DamageDefOf.EMP, this.Props.EMPDamageAmount, -1f); + } + this.TargetTakeDamage(casterPawn, this.pawnConduct[i + 1], this.Props.damageDef, (float)damageMultiplier, armorPenetrationMultiplier); + if (this.verbProps.beamMoteDef != null) + { + MoteMaker.MakeInteractionOverlay(this.verbProps.beamMoteDef, new TargetInfo(this.pawnConduct[i].Position, this.caster.Map, false), new TargetInfo(this.pawnConduct[i + 1].Position, this.caster.Map, false)); + } + } + } + // 否则(非导电模式,或没有传导目标),执行一次普通的单体攻击 + else + { + Thing primaryTarget = this.currentTarget.Thing; + if (primaryTarget != null) + { + float angle = (primaryTarget.Position - this.caster.Position).AngleFlat; + DamageInfo dinfo = new DamageInfo(this.Props.damageDef, (float)damageMultiplier, armorPenetrationMultiplier, angle, this.caster, null, base.EquipmentSource.def, DamageInfo.SourceCategory.ThingOrUnknown, this.currentTarget.Thing); + primaryTarget.TakeDamage(dinfo); + } + + // 无论是否命中,都显示视觉效果 + if (this.verbProps.beamMoteDef != null) + { + MoteMaker.MakeInteractionOverlay(this.verbProps.beamMoteDef, new TargetInfo(this.caster.Position, this.caster.Map, false), new TargetInfo(this.currentTarget.Cell, this.caster.Map, false)); + } + } + this.pawnConduct.Clear(); + } + + private void DoExplosion(Pawn casterPawn, int damAmount, float armorPenetration, FloatRange? affectedAngle) + { + GenExplosion.DoExplosion( + center: casterPawn.Position, + map: this.caster.MapHeld, + radius: this.verbProps.range, + damType: this.Props.damageDef, + instigator: casterPawn, // Corrected + damAmount: damAmount, + armorPenetration: armorPenetration, + explosionSound: null, + weapon: this.CasterPawn.equipment?.Primary?.def, // Safety check + projectile: null, + intendedTarget: this.currentTarget.Thing, // Corrected + postExplosionSpawnThingDef: null, // Simplified + postExplosionSpawnChance: 0f, + postExplosionSpawnThingCount: 1, + postExplosionGasType: null, + postExplosionGasRadiusOverride: null, + postExplosionGasAmount: 0, + applyDamageToExplosionCellsNeighbors: false, + preExplosionSpawnThingDef: null, + preExplosionSpawnChance: 0f, + preExplosionSpawnThingCount: 1, + chanceToStartFire: 0f, + damageFalloff: false, + direction: null, + ignoredThings: null, + affectedAngle: affectedAngle, + doVisualEffects: true, + propagationSpeed: 0.6f, + excludeRadius: 0f, + doSoundEffects: false, + postExplosionSpawnThingDefWater: null, + screenShakeFactor: 1f, + flammabilityChanceCurve: null, + overrideCells: null, + postExplosionSpawnSingleThingDef: null, + preExplosionSpawnSingleThingDef: null + ); + } + + + public override void DrawHighlight(LocalTargetInfo target) + { + base.DrawHighlight(target); + bool isValid = target.IsValid; + bool flag = isValid; + if (flag) + { + IntVec3 position = this.caster.Position; + float num = Mathf.Atan2(-(float)(target.Cell.z - position.z), (float)(target.Cell.x - position.x)) * 57.29578f; + Verb_ShootArc.RenderPredictedAreaOfEffect(this.caster.Position, this.Props.range, this.verbProps.explosionRadiusRingColor, new FloatRange(num - this.Props.affectedAngle, num + this.Props.affectedAngle)); + } + } + + public static void RenderPredictedAreaOfEffect(IntVec3 loc, float radius, Color color, FloatRange affectedAngle) + { + bool flag = affectedAngle.min < -180f || affectedAngle.max > 180f; + bool flag2 = flag; + List cellsSum; + if (flag2) + { + DamageWorker worker = DamageDefOf.Bomb.Worker; + Map currentMap = Find.CurrentMap; + FloatRange? affectedAngle2 = new FloatRange?(new FloatRange(Verb_ShootArc.AngleWrapped(affectedAngle.min), 180f)); + List cells = worker.ExplosionCellsToHit(loc, currentMap, radius, null, null, affectedAngle2).ToList(); + DamageWorker worker2 = DamageDefOf.Bomb.Worker; + Map currentMap2 = Find.CurrentMap; + affectedAngle2 = new FloatRange?(new FloatRange(-180f, Verb_ShootArc.AngleWrapped(affectedAngle.max))); + List cells2 = worker2.ExplosionCellsToHit(loc, currentMap2, radius, null, null, affectedAngle2).ToList(); + cellsSum = cells.Concat(cells2).ToList(); + } + else + { + DamageWorker worker3 = DamageDefOf.Bomb.Worker; + Map currentMap3 = Find.CurrentMap; + FloatRange? affectedAngle3 = new FloatRange?(affectedAngle); + cellsSum = worker3.ExplosionCellsToHit(loc, currentMap3, radius, null, null, affectedAngle3).ToList(); + } + GenDraw.DrawFieldEdges(cellsSum, color, null); + } + + public static float AngleWrapped(float angle) + { + while (angle > 180f) + { + angle -= 360f; + } + while (angle < -180f) + { + angle += 360f; + } + return (angle == 180f) ? -180f : angle; + } + + private static IEnumerable circularSectorCellsStartedCaster(IntVec3 center, Map map, IntVec3 target, float radius, float angle, bool useCenter = false) + { + float num = Mathf.Atan2(-(float)(target.z - center.z), (float)(target.x - center.x)) * 57.29578f; + FloatRange affectedAngle = new FloatRange(num - angle, num + angle); + bool flag = affectedAngle.min < -180f || affectedAngle.max > 180f; + bool flag2 = flag; + List cellsSum; + if (flag2) + { + DamageWorker worker = DamageDefOf.Bomb.Worker; + FloatRange? affectedAngle2 = new FloatRange?(new FloatRange(Verb_ShootArc.AngleWrapped(affectedAngle.min), 180f)); + List cells = worker.ExplosionCellsToHit(center, map, radius, null, null, affectedAngle2).ToList(); + DamageWorker worker2 = DamageDefOf.Bomb.Worker; + affectedAngle2 = new FloatRange?(new FloatRange(-180f, Verb_ShootArc.AngleWrapped(affectedAngle.max))); + List cells2 = worker2.ExplosionCellsToHit(center, map, radius, null, null, affectedAngle2).ToList(); + cellsSum = cells.Concat(cells2).ToList(); + } + else + { + DamageWorker worker3 = DamageDefOf.Bomb.Worker; + FloatRange? affectedAngle3 = new FloatRange?(affectedAngle); + cellsSum = worker3.ExplosionCellsToHit(center, map, radius, null, null, affectedAngle3).ToList(); + } + return cellsSum; + } + + protected virtual HashSet HashSetConverter(IEnumerable points) + { + HashSet hashSet = new HashSet(); + bool flag = points.Any(); + bool flag2 = flag; + if (flag2) + { + foreach (IntVec3 point in points) + { + hashSet.Add(point); + } + } + return hashSet; + } + + private void TargetTakeDamage(Pawn caster, Pawn target, DamageDef damageDef, float damageAmount, float armorPenetration = -1f) + { + bool flag = caster == null || target == null; + bool flag2 = flag; + if (flag2) + { + Log.Error("TargetTakeDamage has null caster or target"); + } + else + { + float angleFlat = (this.currentTarget.Cell - caster.Position).AngleFlat; + BattleLogEntry_RangedImpact log = new BattleLogEntry_RangedImpact(caster, target, this.currentTarget.Thing, base.EquipmentSource.def, null, null); + DamageInfo dinfo = new DamageInfo(damageDef, damageAmount, armorPenetration, angleFlat, caster, null, base.EquipmentSource.def, DamageInfo.SourceCategory.ThingOrUnknown, this.currentTarget.Thing, true, true, QualityCategory.Normal, true); + target.TakeDamage(dinfo).AssociateWithLog(log); + } + } + + public int GetDamageAmount(float weaponDamageMultiplier, StringBuilder explanation = null) + { + int num = this.damageAmount; + bool flag = explanation != null; + bool flag3 = flag; + if (flag3) + { + explanation.AppendLine("StatsReport_BaseValue".Translate() + ": " + num.ToString()); + explanation.Append("StatsReport_QualityMultiplier".Translate() + ": " + weaponDamageMultiplier.ToStringPercent()); + } + num = Mathf.RoundToInt((float)num * weaponDamageMultiplier); + bool flag2 = explanation != null; + bool flag4 = flag2; + if (flag4) + { + explanation.AppendLine(); + explanation.AppendLine(); + explanation.Append("StatsReport_FinalValue".Translate() + ": " + num.ToString()); + } + return num; + } + + public float GetArmorPenetration(float weaponDamageMultiplier, StringBuilder explanation = null) + { + float num = this.armorPenetration; + bool flag = num < 0f; + bool flag4 = flag; + if (flag4) + { + num = (float)this.damageAmount * 0.015f; + } + bool flag2 = explanation != null; + bool flag5 = flag2; + if (flag5) + { + explanation.AppendLine("StatsReport_BaseValue".Translate() + ": " + num.ToStringPercent()); + explanation.AppendLine(); + explanation.Append("StatsReport_QualityMultiplier".Translate() + ": " + weaponDamageMultiplier.ToStringPercent()); + } + num *= weaponDamageMultiplier; + bool flag3 = explanation != null; + bool flag6 = flag3; + if (flag6) + { + explanation.AppendLine(); + explanation.AppendLine(); + explanation.Append("StatsReport_FinalValue".Translate() + ": " + num.ToStringPercent()); + } + return num; + } + + public List pawnConduct = new List(); + } +} + diff --git a/Source/ArachnaeSwarm/Verb/Verb_ShootBeamExplosive.cs b/Source/ArachnaeSwarm/Verb/Verb_ShootBeamExplosive.cs new file mode 100644 index 0000000..fe2175e --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/Verb_ShootBeamExplosive.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace ArachnaeSwarm +{ + public class Verb_ShootBeamExplosive : Verse.Verb_ShootBeam + { + private int explosionShotCounter = 0; + + protected override bool TryCastShot() + { + bool result = base.TryCastShot(); + + if (result && verbProps is VerbPropertiesExplosiveBeam explosiveProps && explosiveProps.enableExplosion) + { + explosionShotCounter++; + + if (explosionShotCounter >= explosiveProps.explosionShotInterval) + { + explosionShotCounter = 0; + TriggerExplosion(explosiveProps); + } + } + + return result; + } + + private void TriggerExplosion(VerbPropertiesExplosiveBeam explosiveProps) + { + Vector3 explosionPos = InterpolatedPosition; + IntVec3 explosionCell = explosionPos.ToIntVec3(); + + if (!explosionCell.InBounds(caster.Map)) + return; + + // 播放爆炸音效 + if (explosiveProps.explosionSound != null) + { + explosiveProps.explosionSound.PlayOneShot(new TargetInfo(explosionCell, caster.Map)); + } + + // 生成爆炸 + GenExplosion.DoExplosion( + center: explosionCell, + map: caster.Map, + radius: explosiveProps.explosionRadius, + damType: explosiveProps.explosionDamageDef ?? DamageDefOf.Bomb, + instigator: caster, + damAmount: explosiveProps.explosionDamage > 0 ? explosiveProps.explosionDamage : verbProps.defaultProjectile?.projectile?.GetDamageAmount(EquipmentSource) ?? 20, + armorPenetration: explosiveProps.explosionArmorPenetration >= 0 ? explosiveProps.explosionArmorPenetration : verbProps.defaultProjectile?.projectile?.GetArmorPenetration(EquipmentSource) ?? 0.3f, + explosionSound: null, // 我们已经手动播放了音效 + weapon: base.EquipmentSource?.def, + projectile: null, + intendedTarget: currentTarget.Thing, + postExplosionSpawnThingDef: explosiveProps.postExplosionSpawnThingDef, + postExplosionSpawnChance: explosiveProps.postExplosionSpawnChance, + postExplosionSpawnThingCount: explosiveProps.postExplosionSpawnThingCount, + postExplosionGasType: explosiveProps.postExplosionGasType, + applyDamageToExplosionCellsNeighbors: explosiveProps.applyDamageToExplosionCellsNeighbors, + preExplosionSpawnThingDef: explosiveProps.preExplosionSpawnThingDef, + preExplosionSpawnChance: explosiveProps.preExplosionSpawnChance, + preExplosionSpawnThingCount: explosiveProps.preExplosionSpawnThingCount, + chanceToStartFire: explosiveProps.chanceToStartFire, + damageFalloff: explosiveProps.damageFalloff, + direction: null, + ignoredThings: null, + affectedAngle: null, + doVisualEffects: true, + propagationSpeed: 0.6f, + excludeRadius: 0f, + doSoundEffects: false, // 我们手动处理音效 + screenShakeFactor: explosiveProps.screenShakeFactor // 新增:屏幕震动因子 + ); + + // 生成额外的视觉效果 + if (explosiveProps.explosionEffecter != null) + { + Effecter effecter = explosiveProps.explosionEffecter.Spawn(explosionCell, caster.Map); + effecter.Trigger(new TargetInfo(explosionCell, caster.Map), TargetInfo.Invalid); + effecter.Cleanup(); + } + } + + public override void ExposeData() + { + base.ExposeData(); + Scribe_Values.Look(ref explosionShotCounter, "explosionShotCounter", 0); + } + } +} diff --git a/Source/ArachnaeSwarm/Verb/Verb_ShootMeltBeam.cs b/Source/ArachnaeSwarm/Verb/Verb_ShootMeltBeam.cs new file mode 100644 index 0000000..0709e93 --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/Verb_ShootMeltBeam.cs @@ -0,0 +1,585 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.Sound; +namespace ArachnaeSwarm +{ + // 我们让它继承自我们自己的 Verb_ShootBeamExplosive 以便复用爆炸逻辑 + public class Verb_ShootMeltBeam : Verb + { + // --- 从 Verb_ShootBeamExplosive 复制过来的字段 --- + private int explosionShotCounter = 0; + private int mirroredExplosionShotCounter = 0; + // --------------------------------------------- + + protected override int ShotsPerBurst + { + get + { + return this.verbProps.burstShotCount; + } + } + + public float ShotProgress + { + get + { + return (float)this.ticksToNextPathStep / (float)this.verbProps.ticksBetweenBurstShots; + } + } + + public Vector3 InterpolatedPosition + { + get + { + Vector3 b = base.CurrentTarget.CenterVector3 - this.initialTargetPosition; + return Vector3.Lerp(this.path[this.burstShotsLeft], this.path[Mathf.Min(this.burstShotsLeft + 1, this.path.Count - 1)], this.ShotProgress) + b; + } + } + + // 为镜像光束添加一个计算位置的属性 + public Vector3 MirroredInterpolatedPosition + { + get + { + Vector3 b = base.CurrentTarget.CenterVector3 - this.initialTargetPosition; + return Vector3.Lerp(this.mirroredPath[this.burstShotsLeft], this.mirroredPath[Mathf.Min(this.burstShotsLeft + 1, this.mirroredPath.Count - 1)], this.ShotProgress) + b; + } + } + + public override float? AimAngleOverride + { + get + { + return (this.state != VerbState.Bursting) ? null : new float?((this.InterpolatedPosition - this.caster.DrawPos).AngleFlat()); + } + } + + public override void DrawHighlight(LocalTargetInfo target) + { + base.DrawHighlight(target); + this.CalculatePath(target.CenterVector3, this.tmpPath, this.tmpPathCells, false); + foreach (IntVec3 tmpPathCell in this.tmpPathCells) + { + ShootLine resultingLine; + bool flag = this.TryFindShootLineFromTo(this.caster.Position, target, out resultingLine); + if ((this.verbProps.stopBurstWithoutLos && !flag) || !this.TryGetHitCell(resultingLine.Source, tmpPathCell, out var hitCell)) + { + continue; + } + this.tmpHighlightCells.Add(hitCell); + if (!this.verbProps.beamHitsNeighborCells) + { + continue; + } + foreach (IntVec3 beamHitNeighbourCell in this.GetBeamHitNeighbourCells(resultingLine.Source, hitCell)) + { + if (!this.tmpHighlightCells.Contains(beamHitNeighbourCell)) + { + this.tmpSecondaryHighlightCells.Add(beamHitNeighbourCell); + } + } + } + this.tmpSecondaryHighlightCells.RemoveWhere((IntVec3 x) => this.tmpHighlightCells.Contains(x)); + if (this.tmpHighlightCells.Any()) + { + GenDraw.DrawFieldEdges(this.tmpHighlightCells.ToList(), this.verbProps.highlightColor ?? Color.white); + } + if (this.tmpSecondaryHighlightCells.Any()) + { + GenDraw.DrawFieldEdges(this.tmpSecondaryHighlightCells.ToList(), this.verbProps.secondaryHighlightColor ?? Color.white); + } + this.tmpHighlightCells.Clear(); + this.tmpSecondaryHighlightCells.Clear(); + } + + protected override bool TryCastShot() + { + bool flag = this.currentTarget.HasThing && this.currentTarget.Thing.Map != this.caster.Map; + bool result; + if (flag) + { + result = false; + } + else + { + ShootLine shootLine; + bool flag2 = base.TryFindShootLineFromTo(this.caster.Position, this.currentTarget, out shootLine, false); + bool flag3 = this.verbProps.stopBurstWithoutLos && !flag2; + if (flag3) + { + result = false; + } + else + { + bool flag4 = base.EquipmentSource != null; + if (flag4) + { + CompChangeableProjectile comp = base.EquipmentSource.GetComp(); + if (comp != null) + { + comp.Notify_ProjectileLaunched(); + } + CompApparelReloadable comp2 = base.EquipmentSource.GetComp(); + if (comp2 != null) + { + comp2.UsedOnce(); + } + } + this.lastShotTick = Find.TickManager.TicksGame; + this.ticksToNextPathStep = this.verbProps.ticksBetweenBurstShots; + IntVec3 targetCell = this.InterpolatedPosition.Yto0().ToIntVec3(); + IntVec3 intVec; + bool flag5 = !this.TryGetHitCell(shootLine.Source, targetCell, out intVec); + if (flag5) + { + result = true; + } + else + { + this.HitCell(intVec, shootLine.Source, 1f); + bool beamHitsNeighborCells = this.verbProps.beamHitsNeighborCells; + if (beamHitsNeighborCells) + { + this.hitCells.Add(intVec); + foreach (IntVec3 intVec2 in this.GetBeamHitNeighbourCells(shootLine.Source, intVec)) + { + bool flag6 = !this.hitCells.Contains(intVec2); + if (flag6) + { + float damageFactor = this.pathCells.Contains(intVec2) ? 1f : 0.5f; + this.HitCell(intVec2, shootLine.Source, damageFactor); + this.hitCells.Add(intVec2); + } + } + } + IntVec3 targetCell2 = this.mirroredPath[Mathf.Min(this.burstShotsLeft, this.mirroredPath.Count - 1)].ToIntVec3(); + IntVec3 intVec3; + bool flag7 = this.TryGetHitCell(shootLine.Source, targetCell2, out intVec3); + if (flag7) + { + this.HitCell(intVec3, shootLine.Source, 1f); + this.mirroredHitCells.Add(intVec3); + bool beamHitsNeighborCells2 = this.verbProps.beamHitsNeighborCells; + if (beamHitsNeighborCells2) + { + foreach (IntVec3 intVec4 in this.GetBeamHitNeighbourCells(shootLine.Source, intVec3)) + { + bool flag8 = !this.mirroredHitCells.Contains(intVec4); + if (flag8) + { + float damageFactor2 = this.mirroredPathCells.Contains(intVec4) ? 1f : 0.5f; + this.HitCell(intVec4, shootLine.Source, damageFactor2); + this.mirroredHitCells.Add(intVec4); + } + } + } + } + + // --- 添加爆炸逻辑 --- + if (verbProps is VerbPropertiesExplosiveBeam explosiveProps && explosiveProps.enableExplosion) + { + explosionShotCounter++; + mirroredExplosionShotCounter++; + + if (explosionShotCounter >= explosiveProps.explosionShotInterval) + { + explosionShotCounter = 0; + TriggerExplosion(explosiveProps, InterpolatedPosition); + } + if (mirroredExplosionShotCounter >= explosiveProps.explosionShotInterval) + { + mirroredExplosionShotCounter = 0; + TriggerExplosion(explosiveProps, MirroredInterpolatedPosition); + } + } + // --------------------- + + result = true; + } + } + } + return result; + } + + // --- 从 Verb_ShootBeamExplosive 复制过来的方法 --- + private void TriggerExplosion(VerbPropertiesExplosiveBeam explosiveProps, Vector3 position) + { + IntVec3 explosionCell = position.ToIntVec3(); + + if (!explosionCell.InBounds(caster.Map)) + return; + + // 播放爆炸音效 + if (explosiveProps.explosionSound != null) + { + explosiveProps.explosionSound.PlayOneShot(new TargetInfo(explosionCell, caster.Map)); + } + + // 生成爆炸 + GenExplosion.DoExplosion( + center: explosionCell, + map: caster.Map, + radius: explosiveProps.explosionRadius, + damType: explosiveProps.explosionDamageDef ?? DamageDefOf.Bomb, + instigator: caster, + damAmount: explosiveProps.explosionDamage > 0 ? explosiveProps.explosionDamage : verbProps.defaultProjectile?.projectile?.GetDamageAmount(EquipmentSource) ?? 20, + armorPenetration: explosiveProps.explosionArmorPenetration >= 0 ? explosiveProps.explosionArmorPenetration : verbProps.defaultProjectile?.projectile?.GetArmorPenetration(EquipmentSource) ?? 0.3f, + explosionSound: null, // 我们已经手动播放了音效 + weapon: base.EquipmentSource?.def, + projectile: null, + intendedTarget: currentTarget.Thing, + postExplosionSpawnThingDef: explosiveProps.postExplosionSpawnThingDef, + postExplosionSpawnChance: explosiveProps.postExplosionSpawnChance, + postExplosionSpawnThingCount: explosiveProps.postExplosionSpawnThingCount, + postExplosionGasType: explosiveProps.postExplosionGasType, + applyDamageToExplosionCellsNeighbors: explosiveProps.applyDamageToExplosionCellsNeighbors, + preExplosionSpawnThingDef: explosiveProps.preExplosionSpawnThingDef, + preExplosionSpawnChance: explosiveProps.preExplosionSpawnChance, + preExplosionSpawnThingCount: explosiveProps.preExplosionSpawnThingCount, + chanceToStartFire: explosiveProps.chanceToStartFire, + damageFalloff: explosiveProps.damageFalloff, + direction: null, + ignoredThings: null, + affectedAngle: null, + doVisualEffects: true, + propagationSpeed: 0.6f, + excludeRadius: 0f, + doSoundEffects: false, // 我们手动处理音效 + screenShakeFactor: explosiveProps.screenShakeFactor // 新增:屏幕震动因子 + ); + + // 生成额外的视觉效果 + if (explosiveProps.explosionEffecter != null) + { + Effecter effecter = explosiveProps.explosionEffecter.Spawn(explosionCell, caster.Map); + effecter.Trigger(new TargetInfo(explosionCell, caster.Map), TargetInfo.Invalid); + effecter.Cleanup(); + } + } + // --------------------------------------------- + + + protected bool TryGetHitCell(IntVec3 source, IntVec3 targetCell, out IntVec3 hitCell) + { + IntVec3 intVec = GenSight.LastPointOnLineOfSight(source, targetCell, (IntVec3 c) => c.InBounds(this.caster.Map) && c.CanBeSeenOverFast(this.caster.Map), true); + bool flag = this.verbProps.beamCantHitWithinMinRange && (double)intVec.DistanceTo(source) < (double)this.verbProps.minRange; + bool result; + if (flag) + { + hitCell = default(IntVec3); + result = false; + } + else + { + hitCell = (intVec.IsValid ? intVec : targetCell); + result = intVec.IsValid; + } + return result; + } + + protected IntVec3 GetHitCell(IntVec3 source, IntVec3 targetCell) + { + IntVec3 result; + this.TryGetHitCell(source, targetCell, out result); + return result; + } + + protected IEnumerable GetBeamHitNeighbourCells(IntVec3 source, IntVec3 pos) + { + // 重写反编译的迭代器方法以修复编译错误 + for (int i = 0; i < GenAdj.AdjacentCells.Length; i++) + { + IntVec3 cell = pos + GenAdj.AdjacentCells[i]; + if (cell.InBounds(this.caster.Map)) + { + yield return cell; + } + } + } + + public override bool TryStartCastOn(LocalTargetInfo castTarg, LocalTargetInfo destTarg, bool surpriseAttack = false, bool canHitNonTargetPawns = true, bool preventFriendlyFire = false, bool nonInterruptingSelfCast = false) + { + return base.TryStartCastOn(this.verbProps.beamTargetsGround ? castTarg.Cell : castTarg, destTarg, surpriseAttack, canHitNonTargetPawns, preventFriendlyFire, nonInterruptingSelfCast); + } + + private void UpdateBeamVisuals(List path, MoteDualAttached mote, ref Effecter endEffecter, Vector3 casterPos, IntVec3 casterCell, bool isMirrored = false) + { + Vector3 vector = path[Mathf.Min(this.burstShotsLeft, path.Count - 1)]; + Vector3 v = (vector - casterPos).Yto0(); + float num = v.MagnitudeHorizontal(); + Vector3 normalized = v.normalized; + IntVec3 intVec = vector.ToIntVec3(); + IntVec3 b = GenSight.LastPointOnLineOfSight(casterCell, intVec, (IntVec3 c) => c.CanBeSeenOverFast(this.caster.Map), true); + bool isValid = b.IsValid; + if (isValid) + { + num -= (intVec - b).LengthHorizontal; + vector = casterCell.ToVector3Shifted() + normalized * num; + intVec = vector.ToIntVec3(); + } + Vector3 offsetA = normalized * this.verbProps.beamStartOffset; + Vector3 vector2 = vector - intVec.ToVector3Shifted(); + if (mote != null) + { + mote.UpdateTargets(new TargetInfo(casterCell, this.caster.Map, false), new TargetInfo(intVec, this.caster.Map, false), offsetA, vector2); + } + if (mote != null) + { + mote.Maintain(); + } + bool flag = this.verbProps.beamGroundFleckDef != null && Rand.Chance(this.verbProps.beamFleckChancePerTick); + if (flag) + { + FleckMaker.Static(vector, this.caster.Map, this.verbProps.beamGroundFleckDef, 1f); + } + bool flag2 = endEffecter == null && this.verbProps.beamEndEffecterDef != null; + if (flag2) + { + endEffecter = this.verbProps.beamEndEffecterDef.Spawn(intVec, this.caster.Map, vector2, 1f); + } + bool flag3 = endEffecter != null; + if (flag3) + { + endEffecter.offset = vector2; + endEffecter.EffectTick(new TargetInfo(intVec, this.caster.Map, false), TargetInfo.Invalid); + endEffecter.ticksLeft--; + } + bool flag4 = this.verbProps.beamLineFleckDef != null; + if (flag4) + { + float num2 = num; + int num3 = 0; + while ((float)num3 < num2) + { + bool flag5 = Rand.Chance(this.verbProps.beamLineFleckChanceCurve.Evaluate((float)num3 / num2)); + if (flag5) + { + Vector3 loc = casterPos + (float)num3 * normalized - normalized * Rand.Value + normalized / 2f; + FleckMaker.Static(loc, this.caster.Map, this.verbProps.beamLineFleckDef, 1f); + } + num3++; + } + } + } + + public override void BurstingTick() + { + this.ticksToNextPathStep--; + this.UpdateBeamVisuals(this.path, this.mote, ref this.endEffecter, this.caster.Position.ToVector3Shifted(), this.caster.Position, false); + this.UpdateBeamVisuals(this.mirroredPath, this.mirroredMote, ref this.mirroredEndEffecter, this.caster.Position.ToVector3Shifted(), this.caster.Position, true); + Sustainer sustainer = this.sustainer; + if (sustainer != null) + { + sustainer.Maintain(); + } + } + + public override void WarmupComplete() + { + this.burstShotsLeft = this.ShotsPerBurst; + this.state = VerbState.Bursting; + this.initialTargetPosition = this.currentTarget.CenterVector3; + this.CalculatePath(this.currentTarget.CenterVector3, this.path, this.pathCells, true); + Vector3 normalized = (this.currentTarget.CenterVector3 - this.caster.Position.ToVector3Shifted()).Yto0().normalized; + float angle = 3f; + Vector3 a = normalized.RotatedBy(angle); + float magnitude = (this.currentTarget.CenterVector3 - this.caster.Position.ToVector3Shifted()).magnitude; + Vector3 target = this.caster.Position.ToVector3Shifted() + a * magnitude; + this.CalculatePath(target, this.mirroredPath, this.mirroredPathCells, true); + this.mirroredPath.Reverse(); + this.hitCells.Clear(); + this.mirroredHitCells.Clear(); + bool flag = this.verbProps.beamMoteDef != null; + if (flag) + { + this.mote = MoteMaker.MakeInteractionOverlay(this.verbProps.beamMoteDef, this.caster, new TargetInfo(this.path[0].ToIntVec3(), this.caster.Map, false)); + } + bool flag2 = this.verbProps.beamMoteDef != null; + if (flag2) + { + this.mirroredMote = MoteMaker.MakeInteractionOverlay(this.verbProps.beamMoteDef, this.caster, new TargetInfo(this.mirroredPath[0].ToIntVec3(), this.caster.Map, false)); + } + base.TryCastNextBurstShot(); + this.ticksToNextPathStep = this.verbProps.ticksBetweenBurstShots; + Effecter effecter = this.endEffecter; + if (effecter != null) + { + effecter.Cleanup(); + } + Effecter effecter2 = this.mirroredEndEffecter; + if (effecter2 != null) + { + effecter2.Cleanup(); + } + bool flag3 = this.verbProps.soundCastBeam == null; + if (!flag3) + { + this.sustainer = this.verbProps.soundCastBeam.TrySpawnSustainer(SoundInfo.InMap(this.caster, MaintenanceType.PerTick)); + } + } + + private void CalculatePath(Vector3 target, List pathList, HashSet pathCellsList, bool addRandomOffset = true) + { + pathList.Clear(); + Vector3 vector = (target - this.caster.Position.ToVector3Shifted()).Yto0(); + float magnitude = vector.magnitude; + Vector3 normalized = vector.normalized; + Vector3 a = normalized.RotatedBy(-90f); + float num = ((double)this.verbProps.beamFullWidthRange > 0.0) ? Mathf.Min(magnitude / this.verbProps.beamFullWidthRange, 1f) : 1f; + float d = (this.verbProps.beamWidth + 1f) * num / (float)this.ShotsPerBurst; + Vector3 vector2 = target.Yto0() - a * this.verbProps.beamWidth / 2f * num; + pathList.Add(vector2); + for (int i = 0; i < this.ShotsPerBurst; i++) + { + Vector3 a2 = normalized * (Rand.Value * this.verbProps.beamMaxDeviation) - normalized / 2f; + Vector3 vector3 = Mathf.Sin((float)(((double)i / (double)this.ShotsPerBurst + 0.5) * 3.1415927410125732 * 57.295780181884766)) * this.verbProps.beamCurvature * -normalized - normalized * this.verbProps.beamMaxDeviation / 2f; + if (addRandomOffset) + { + pathList.Add(vector2 + (a2 + vector3) * num); + } + else + { + pathList.Add(vector2 + vector3 * num); + } + vector2 += a * d; + } + pathCellsList.Clear(); + foreach (Vector3 vect in pathList) + { + pathCellsList.Add(vect.ToIntVec3()); + } + } + + private bool CanHit(Thing thing) + { + return thing.Spawned && !CoverUtility.ThingCovered(thing, this.caster.Map); + } + + private void HitCell(IntVec3 cell, IntVec3 sourceCell, float damageFactor = 1f) + { + bool flag = !cell.InBounds(this.caster.Map); + if (!flag) + { + this.ApplyDamage(VerbUtility.ThingsToHit(cell, this.caster.Map, new Func(this.CanHit)).RandomElementWithFallback(null), sourceCell, damageFactor); + bool flag2 = !this.verbProps.beamSetsGroundOnFire || !Rand.Chance(this.verbProps.beamChanceToStartFire); + if (!flag2) + { + FireUtility.TryStartFireIn(cell, this.caster.Map, 1f, this.caster, null); + } + } + } + + private void ApplyDamage(Thing thing, IntVec3 sourceCell, float damageFactor = 1f) + { + IntVec3 intVec = this.InterpolatedPosition.Yto0().ToIntVec3(); + IntVec3 intVec2 = GenSight.LastPointOnLineOfSight(sourceCell, intVec, (IntVec3 c) => c.InBounds(this.caster.Map) && c.CanBeSeenOverFast(this.caster.Map), true); + bool isValid = intVec2.IsValid; + if (isValid) + { + intVec = intVec2; + } + Map map = this.caster.Map; + bool flag = thing == null || this.verbProps.beamDamageDef == null; + if (!flag) + { + Pawn pawn = thing as Pawn; + bool flag2 = pawn != null && pawn.Faction == this.Caster.Faction; + if (!flag2) + { + float angleFlat = (this.currentTarget.Cell - this.caster.Position).AngleFlat; + BattleLogEntry_RangedImpact log = new BattleLogEntry_RangedImpact(this.caster, thing, this.currentTarget.Thing, base.EquipmentSource.def, null, null); + DamageInfo dinfo = ((double)this.verbProps.beamTotalDamage <= 0.0) ? new DamageInfo(this.verbProps.beamDamageDef, (float)this.verbProps.beamDamageDef.defaultDamage * damageFactor, this.verbProps.beamDamageDef.defaultArmorPenetration, angleFlat, this.caster, null, base.EquipmentSource.def, DamageInfo.SourceCategory.ThingOrUnknown, this.currentTarget.Thing, true, true, QualityCategory.Normal, true, false) : new DamageInfo(this.verbProps.beamDamageDef, this.verbProps.beamTotalDamage / (float)this.pathCells.Count * damageFactor, this.verbProps.beamDamageDef.defaultArmorPenetration, angleFlat, this.caster, null, base.EquipmentSource.def, DamageInfo.SourceCategory.ThingOrUnknown, this.currentTarget.Thing, true, true, QualityCategory.Normal, true, false); + thing.TakeDamage(dinfo).AssociateWithLog(log); + bool flag3 = thing.CanEverAttachFire(); + if (flag3) + { + bool flag4 = !Rand.Chance((this.verbProps.flammabilityAttachFireChanceCurve == null) ? this.verbProps.beamChanceToAttachFire : this.verbProps.flammabilityAttachFireChanceCurve.Evaluate(thing.GetStatValue(StatDefOf.Flammability, true, -1))); + if (flag4) + { + return; + } + thing.TryAttachFire(this.verbProps.beamFireSizeRange.RandomInRange, this.caster); + } + else + { + bool flag5 = !Rand.Chance(this.verbProps.beamChanceToStartFire); + if (flag5) + { + return; + } + FireUtility.TryStartFireIn(intVec, map, this.verbProps.beamFireSizeRange.RandomInRange, this.caster, this.verbProps.flammabilityAttachFireChanceCurve); + } + // 移除了热射病和蒸发逻辑 + } + } + } + + public override void ExposeData() + { + base.ExposeData(); + Scribe_Collections.Look(ref this.path, "path", LookMode.Value, Array.Empty()); + Scribe_Values.Look(ref this.ticksToNextPathStep, "ticksToNextPathStep", 0, false); + Scribe_Values.Look(ref this.initialTargetPosition, "initialTargetPosition", default(Vector3), false); + Scribe_Collections.Look(ref this.mirroredPath, "mirroredPath", LookMode.Value, Array.Empty()); + // --- 添加爆炸计数器的保存 --- + Scribe_Values.Look(ref explosionShotCounter, "explosionShotCounter", 0); + Scribe_Values.Look(ref mirroredExplosionShotCounter, "mirroredExplosionShotCounter", 0); + // ------------------------- + bool flag = Scribe.mode == LoadSaveMode.PostLoadInit; + if (flag) + { + bool flag2 = this.path == null; + if (flag2) + { + this.path = new List(); + } + bool flag3 = this.mirroredPath == null; + if (flag3) + { + this.mirroredPath = new List(); + } + } + } + + private List path = new List(); + + private List tmpPath = new List(); + + private int ticksToNextPathStep; + + private Vector3 initialTargetPosition; + + private MoteDualAttached mote; + + private Effecter endEffecter; + + + private Sustainer sustainer; + + private HashSet pathCells = new HashSet(); + + private HashSet tmpPathCells = new HashSet(); + + private HashSet tmpHighlightCells = new HashSet(); + + private HashSet tmpSecondaryHighlightCells = new HashSet(); + + private HashSet hitCells = new HashSet(); + + private const int NumSubdivisionsPerUnitLength = 1; + + private List mirroredPath = new List(); + + private HashSet mirroredPathCells = new HashSet(); + + private HashSet mirroredHitCells = new HashSet(); + + private MoteDualAttached mirroredMote; + + private Effecter mirroredEndEffecter; + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Verb/Verb_ShootShotgun.cs b/Source/ArachnaeSwarm/Verb/Verb_ShootShotgun.cs new file mode 100644 index 0000000..a3f5989 --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/Verb_ShootShotgun.cs @@ -0,0 +1,63 @@ +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Verse; + +namespace ArachnaeSwarm +{ + public class Verb_ShootShotgun : Verb_LaunchProjectile + { + protected override int ShotsPerBurst + { + get + { + return this.verbProps.burstShotCount; + } + } + + public override void WarmupComplete() + { + base.WarmupComplete(); + Pawn pawn = this.currentTarget.Thing as Pawn; + if (pawn != null && !pawn.Downed && this.CasterIsPawn && this.CasterPawn.skills != null) + { + float num = pawn.HostileTo(this.caster) ? 170f : 20f; + float num2 = this.verbProps.AdjustedFullCycleTime(this, this.CasterPawn); + this.CasterPawn.skills.Learn(SkillDefOf.Shooting, num * num2, false, false); + } + } + + protected override bool TryCastShot() + { + bool flag = base.TryCastShot(); + if (flag && this.CasterIsPawn) + { + this.CasterPawn.records.Increment(RecordDefOf.ShotsFired); + } + ShotgunExtension shotgunExtension = ShotgunExtension.Get(this.verbProps.defaultProjectile); + if (flag && shotgunExtension.pelletCount - 1 > 0) + { + for (int i = 0; i < shotgunExtension.pelletCount - 1; i++) + { + base.TryCastShot(); + } + } + return flag; + } + } + + public class ShotgunExtension : DefModExtension + { + public static ShotgunExtension Get(Def def) + { + return def.GetModExtension() ?? ShotgunExtension.defaultValues; + } + + private static readonly ShotgunExtension defaultValues = new ShotgunExtension(); + + public int pelletCount = 1; + } +} diff --git a/Source/ArachnaeSwarm/Verb/Verb_ShootWeaponStealBeam.cs b/Source/ArachnaeSwarm/Verb/Verb_ShootWeaponStealBeam.cs new file mode 100644 index 0000000..e3dc29a --- /dev/null +++ b/Source/ArachnaeSwarm/Verb/Verb_ShootWeaponStealBeam.cs @@ -0,0 +1,174 @@ +using RimWorld; +using Verse; +using System.Linq; +using System.Collections.Generic; +using UnityEngine; // For Vector3 +using Verse.Sound; // For SoundDef.PlayOneShot +using Verse.AI; // For JobQueue + +namespace ArachnaeSwarm +{ + public class Verb_ShootWeaponStealBeam : Verse.Verb_ShootBeam + { + private int explosionShotCounter = 0; + + protected VerbProperties_WeaponStealBeam StealBeamVerbProps => (VerbProperties_WeaponStealBeam)verbProps; + + protected override bool TryCastShot() + { + bool result = base.TryCastShot(); + + // 如果光束命中,对目标Pawn施加Hediff并检查是否抢夺武器 + if (result && currentTarget.Thing is Pawn targetPawn && targetPawn.RaceProps.Humanlike) // 只对人形Pawn生效 + { + ApplyHediffAndCheckForSteal(targetPawn); + } + + if (result && verbProps is VerbPropertiesExplosiveBeam explosiveProps && explosiveProps.enableExplosion) + { + explosionShotCounter++; + + if (explosionShotCounter >= explosiveProps.explosionShotInterval) + { + explosionShotCounter = 0; + TriggerExplosion(explosiveProps); + } + } + + return result; + } + + private void TriggerExplosion(VerbPropertiesExplosiveBeam explosiveProps) + { + Vector3 explosionPos = InterpolatedPosition; + IntVec3 explosionCell = explosionPos.ToIntVec3(); + + if (!explosionCell.InBounds(caster.Map)) + return; + + // 播放爆炸音效 + if (explosiveProps.explosionSound != null) + { + explosiveProps.explosionSound.PlayOneShot(new TargetInfo(explosionCell, caster.Map)); + } + + // 生成爆炸 + GenExplosion.DoExplosion( + center: explosionCell, + map: caster.Map, + radius: explosiveProps.explosionRadius, + damType: explosiveProps.explosionDamageDef ?? DamageDefOf.Bomb, + instigator: caster, + damAmount: explosiveProps.explosionDamage > 0 ? explosiveProps.explosionDamage : verbProps.defaultProjectile?.projectile?.GetDamageAmount(EquipmentSource) ?? 20, + armorPenetration: explosiveProps.explosionArmorPenetration >= 0 ? explosiveProps.explosionArmorPenetration : verbProps.defaultProjectile?.projectile?.GetArmorPenetration(EquipmentSource) ?? 0.3f, + explosionSound: null, // 我们已经手动播放了音效 + weapon: base.EquipmentSource?.def, + projectile: null, + intendedTarget: currentTarget.Thing, + postExplosionSpawnThingDef: explosiveProps.postExplosionSpawnThingDef, + postExplosionSpawnChance: explosiveProps.postExplosionSpawnChance, + postExplosionSpawnThingCount: explosiveProps.postExplosionSpawnThingCount, + postExplosionGasType: explosiveProps.postExplosionGasType, + applyDamageToExplosionCellsNeighbors: explosiveProps.applyDamageToExplosionCellsNeighbors, + preExplosionSpawnThingDef: explosiveProps.preExplosionSpawnThingDef, + preExplosionSpawnChance: explosiveProps.preExplosionSpawnChance, + preExplosionSpawnThingCount: explosiveProps.preExplosionSpawnThingCount, + chanceToStartFire: explosiveProps.chanceToStartFire, + damageFalloff: explosiveProps.damageFalloff, + direction: null, + ignoredThings: null, + affectedAngle: null, + doVisualEffects: true, + propagationSpeed: 0.6f, + excludeRadius: 0f, + doSoundEffects: false, // 我们手动处理音效 + screenShakeFactor: explosiveProps.screenShakeFactor // 新增:屏幕震动因子 + ); + + // 在这里添加武器抢夺和Hediff施加的逻辑(爆炸命中目标时) + if (currentTarget.Thing is Pawn targetPawn && targetPawn.RaceProps.Humanlike) // 只对人形Pawn生效 + { + ApplyHediffAndCheckForSteal(targetPawn); + } + + // 生成额外的视觉效果 + if (explosiveProps.explosionEffecter != null) + { + Effecter effecter = explosiveProps.explosionEffecter.Spawn(explosionCell, caster.Map); + effecter.Trigger(new TargetInfo(explosionCell, caster.Map), TargetInfo.Invalid); + effecter.Cleanup(); + } + } + + private void ApplyHediffAndCheckForSteal(Pawn targetPawn) + { + if (StealBeamVerbProps.hediffToApply == null) + { + return; + } + + Hediff hediff = targetPawn.health.hediffSet.GetFirstHediffOfDef(StealBeamVerbProps.hediffToApply); + + if (hediff == null) + { + hediff = HediffMaker.MakeHediff(StealBeamVerbProps.hediffToApply, targetPawn); + targetPawn.health.AddHediff(hediff); + } + + hediff.Severity += StealBeamVerbProps.hediffSeverityPerHit; + + if (hediff.Severity >= StealBeamVerbProps.hediffMaxSeverity) + { + TryStealWeapon(targetPawn); + if (StealBeamVerbProps.removeHediffOnSteal) + { + targetPawn.health.RemoveHediff(hediff); + } + } + } + + private void TryStealWeapon(Pawn targetPawn) + { + if (!CasterIsPawn || CasterPawn == null) + { + return; + } + + // 获取目标Pawn的装备武器 + ThingWithComps targetWeapon = targetPawn.equipment?.Primary; + + if (targetWeapon != null) + { + // 将武器从目标Pawn身上移除 + targetPawn.equipment.Remove(targetWeapon); + + // 将武器添加到发射者(宿主)的库存中 + if (!CasterPawn.inventory.innerContainer.TryAdd(targetWeapon)) + { + // 如果无法添加到库存,则尝试丢弃在地上 + GenPlace.TryPlaceThing(targetWeapon, CasterPawn.Position, CasterPawn.Map, ThingPlaceMode.Near); + return; // 如果丢弃了,就不尝试装备了 + } + + // 强制发射者装备该武器,并替换当前武器 (JobDriver_Equip 核心逻辑) + // 强制发射者装备该武器,并替换当前武器 (JobDriver_Equip 核心逻辑) + CasterPawn.equipment.MakeRoomFor(targetWeapon); // 为新装备腾出空间,并处理旧装备 + + // 在 AddEquipment 之前,确保武器不在库存中 + if (CasterPawn.inventory.innerContainer.Contains(targetWeapon)) + { + CasterPawn.inventory.innerContainer.Remove(targetWeapon); + } + + CasterPawn.equipment.AddEquipment(targetWeapon); // 添加装备 + targetWeapon.def.soundInteract?.PlayOneShot(new TargetInfo(CasterPawn.Position, CasterPawn.Map)); // 播放音效 + } + } + + public override void ExposeData() + { + base.ExposeData(); + Scribe_Values.Look(ref explosionShotCounter, "explosionShotCounter", 0); + } + } +} \ No newline at end of file