diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index d47b472..692993c 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/AbilityDefs/ARA_GuardianPsyField_Abilities.xml b/1.6/1.6/Defs/AbilityDefs/ARA_GuardianPsyField_Abilities.xml new file mode 100644 index 0000000..d3fc45c --- /dev/null +++ b/1.6/1.6/Defs/AbilityDefs/ARA_GuardianPsyField_Abilities.xml @@ -0,0 +1,60 @@ + + + + + ARA_GuardianPsyField_On + + 投射一个强大的灵能防御力场。 + ArachnaeSwarm/UI/Abilities/ARA_RaceBaseSwarmProduceOn + 601 + false + true + false + + Verb_CastAbility + 0 + false + false + false + + true + + + +
  • + ArachnaeSwarm.CompAbilityEffect_GiveSwitchHediff + ARA_GuardianPsyField + true +
  • +
    +
    + + + ARA_GuardianPsyField_Off + + 关闭灵能防御力场。 + ArachnaeSwarm/UI/Abilities/ARA_RaceBaseSwarmProduceOff + 601 + false + true + false + + Verb_CastAbility + 0 + false + false + false + + true + + + +
  • + ArachnaeSwarm.CompAbilityEffect_RemoveSwitchHediff + ARA_GuardianPsyField + true +
  • +
    +
    + +
    \ No newline at end of file diff --git a/1.6/1.6/Defs/HediffDefs/ARA_GuardianPsyField_Hediff.xml b/1.6/1.6/Defs/HediffDefs/ARA_GuardianPsyField_Hediff.xml new file mode 100644 index 0000000..c197be1 --- /dev/null +++ b/1.6/1.6/Defs/HediffDefs/ARA_GuardianPsyField_Hediff.xml @@ -0,0 +1,38 @@ + + + + + ARA_GuardianPsyField + + 一个强大的灵能防御力场。它会自动拦截进入其作用范围的敌对飞行物,并能将附近友军受到的伤害转移到施法者身上。每次成功的守护都会消耗施法者的精神力。 + ArachnaeSwarm.Hediff_DynamicInterceptor + false + (0.6, 0.2, 0.9) + false + +
  • + + + 5.9 + 1500 + 3200 + 60 + + + + 0.001 + 0.1 + 0.01 + + + (0.5, 0.3, 0.9, 0.5) + Interceptor_BlockedProjectile + Interceptor_BlockedProjectile + Shield_Break + BulletShieldGenerator_Reactivate + +
  • +
    +
    + +
    \ No newline at end of file diff --git a/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml b/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml index 35d52f8..05e370b 100644 --- a/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml +++ b/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml @@ -291,6 +291,8 @@
  • ARA_PsychicBrainburn
  • ARA_NeuroSwarm_jump
  • +
  • ARA_GuardianPsyField_On
  • +
  • ARA_GuardianPsyField_Off
  • diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj index 40c27db..eb83722 100644 --- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj +++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj @@ -169,6 +169,9 @@ + + + diff --git a/Source/ArachnaeSwarm/Harmony_ProjectileInterceptor.cs b/Source/ArachnaeSwarm/Harmony_ProjectileInterceptor.cs new file mode 100644 index 0000000..43f34bb --- /dev/null +++ b/Source/ArachnaeSwarm/Harmony_ProjectileInterceptor.cs @@ -0,0 +1,41 @@ +using HarmonyLib; +using RimWorld; +using Verse; +using UnityEngine; + +namespace ArachnaeSwarm +{ + [HarmonyPatch(typeof(Projectile), "CheckForFreeInterceptBetween")] + public static class Projectile_CheckForFreeInterceptBetween_Patch + { + // This patch will find our custom ThingComp on pawns and call its intercept method. + public static bool Prefix(Projectile __instance, Vector3 lastExactPos, Vector3 newExactPos) + { + if (__instance.Map == null || __instance.Destroyed) + { + return true; // Let original method run if something is wrong + } + + // Iterate through all pawns on the map + foreach (Pawn pawn in __instance.Map.mapPawns.AllPawnsSpawned) + { + // Our comp is directly on the pawn, not on apparel + if (pawn.TryGetComp(out var interceptor)) + { + // Call our custom intercept method + if (interceptor.TryIntercept(__instance)) + { + // If interception is successful, destroy the projectile + __instance.Destroy(DestroyMode.Vanish); + + // Prevent the original game method from running, as we've handled it + return false; + } + } + } + + // If no interception happened, let the original method run + return true; + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Hediff_DynamicInterceptor.cs b/Source/ArachnaeSwarm/Hediff_DynamicInterceptor.cs new file mode 100644 index 0000000..3c939c3 --- /dev/null +++ b/Source/ArachnaeSwarm/Hediff_DynamicInterceptor.cs @@ -0,0 +1,68 @@ +using Verse; +using RimWorld; +using System.Linq; +using System; // For Activator + +namespace ArachnaeSwarm +{ + public class Hediff_DynamicInterceptor : HediffWithComps + { + public CompProperties_GuardianPsyField GuardianProps + { + get + { + var hediffCompProps = def.comps?.FirstOrDefault(c => c is HediffCompProperties_DynamicInterceptor) as HediffCompProperties_DynamicInterceptor; + return hediffCompProps?.guardianProps; + } + } + + public override void PostAdd(DamageInfo? dinfo) + { + base.PostAdd(dinfo); + var props = GuardianProps; + if (pawn != null && props != null) + { + if (pawn.GetComp() == null) + { + Log.Message($"[DynamicInterceptor] Adding ThingComp_GuardianPsyField to {pawn.LabelShort}."); + var newComp = (ThingComp_GuardianPsyField)Activator.CreateInstance(typeof(ThingComp_GuardianPsyField)); + newComp.parent = pawn; + // Initialize with the actual properties from the HediffDef + newComp.Initialize(props); + pawn.AllComps.Add(newComp); + } + } + } + + public override void PostRemoved() + { + base.PostRemoved(); + if (pawn != null) + { + var comp = pawn.GetComp(); + if (comp != null) + { + Log.Message($"[DynamicInterceptor] Removing ThingComp_GuardianPsyField from {pawn.LabelShort}."); + pawn.AllComps.Remove(comp); + } + } + } + } + + // This comp will hold the properties for our custom interceptor + public class HediffCompProperties_DynamicInterceptor : HediffCompProperties + { + public CompProperties_GuardianPsyField guardianProps; // Nested properties + + public HediffCompProperties_DynamicInterceptor() + { + this.compClass = typeof(HediffComp_DynamicInterceptor); + } + } + + // A simple HediffComp to go with the properties + public class HediffComp_DynamicInterceptor : HediffComp + { + public HediffCompProperties_DynamicInterceptor Props => props as HediffCompProperties_DynamicInterceptor; + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/ThingComp_GuardianPsyField.cs b/Source/ArachnaeSwarm/ThingComp_GuardianPsyField.cs new file mode 100644 index 0000000..d8efac6 --- /dev/null +++ b/Source/ArachnaeSwarm/ThingComp_GuardianPsyField.cs @@ -0,0 +1,320 @@ +using RimWorld; +using Verse; +using UnityEngine; +using Verse.Sound; + +namespace ArachnaeSwarm +{ + // 1. Expanded CompProperties to match the user's example + public class CompProperties_GuardianPsyField : CompProperties + { + public float radius = 5.9f; + public int hitPoints = 100; + public int rechargeDelay = 3200; // Ticks after breaking + public int rechargeHitPointsIntervalTicks = 60; // Ticks to restore 1 HP + + // New properties for psyfocus/entropy mechanics + // Removed psyfocusCostForFullRecharge + public float psyfocusCostPerInterval = 0.001f; // e.g., 0.1% of max psyfocus per recharge interval + public float entropyGainPerDamage = 0.5f; // For self + // Removed entropyGainPerAllyDamage + public float hitPointsPctPerInterval = 0.01f; // Restore 1% of max HP per interval + public EffecterDef absorbEffecter; + // Removed transferAllyDamage + + public EffecterDef interceptEffecter; + public EffecterDef breakEffecter; + public EffecterDef reactivateEffecter; + + public Color color = Color.cyan; + + public CompProperties_GuardianPsyField() + { + compClass = typeof(ThingComp_GuardianPsyField); + } + } + + [StaticConstructorOnStartup] + public class ThingComp_GuardianPsyField : ThingComp + { + // --- State Variables --- + private int lastInterceptTicks = -999999; + private int ticksToReset = 0; // Cooldown timer + public int currentHitPoints; + private bool wasNotAtFullHp = false; // Tracks if shield was damaged before recharge + + // --- Properties --- + public CompProperties_GuardianPsyField Props => (CompProperties_GuardianPsyField)props; + private Pawn PawnOwner => parent as Pawn; + public bool IsOnCooldown => ticksToReset > 0; + public int HitPointsMax => Props.hitPoints; + + // --- Visuals --- + private static readonly Material ForceFieldMat = MaterialPool.MatFrom("Other/ForceField", ShaderDatabase.MoteGlow); + private static readonly MaterialPropertyBlock MatPropertyBlock = new MaterialPropertyBlock(); + + public bool Active + { + get + { + // Shield is active if pawn is valid, hediff is present, not on cooldown, and has psyfocus. + // currentHitPoints <= 0 should NOT prevent activation for recharge. + if (PawnOwner == null || !PawnOwner.Spawned || PawnOwner.Dead || PawnOwner.Downed || IsOnCooldown) + return false; + + var hediff = PawnOwner.health.hediffSet.GetFirstHediffOfDef(HediffDef.Named("ARA_GuardianPsyField")); + if (hediff == null) return false; + + // Shield is only active if pawn has psyfocus + if (PawnOwner.psychicEntropy == null || PawnOwner.psychicEntropy.CurrentPsyfocus <= 0) return false; + + return true; + } + } + + public override void PostPostMake() + { + base.PostPostMake(); + currentHitPoints = HitPointsMax; + } + + public override void PostExposeData() + { + base.PostExposeData(); + Scribe_Values.Look(ref lastInterceptTicks, "lastInterceptTicks", -999999); + Scribe_Values.Look(ref ticksToReset, "ticksToReset", 0); + Scribe_Values.Look(ref currentHitPoints, "currentHitPoints", 0); + } + + public override void CompTick() + { + base.CompTick(); + if (PawnOwner == null) return; + + if (IsOnCooldown) + { + ticksToReset--; + if (ticksToReset <= 0) + { + Reset(); + } + } + // Only allow recharge if the shield is 'Active' (i.e., has psyfocus) and not on cooldown + else if (Active && currentHitPoints < HitPointsMax) + { + // Check if there's enough psyfocus to pay for this interval's recharge + if (PawnOwner.psychicEntropy != null && PawnOwner.psychicEntropy.CurrentPsyfocus >= Props.psyfocusCostPerInterval) + { + wasNotAtFullHp = true; // Mark that the shield was damaged + if(this.parent.IsHashIntervalTick(Props.rechargeHitPointsIntervalTicks)) + { + currentHitPoints += (int)(HitPointsMax * Props.hitPointsPctPerInterval); + if(currentHitPoints > HitPointsMax) currentHitPoints = HitPointsMax; + + // Deduct psyfocus for this interval + PawnOwner.psychicEntropy.OffsetPsyfocusDirectly(-Props.psyfocusCostPerInterval); + } + } + else + { + // Not enough psyfocus to recharge, log for debugging + Log.Message($"[GuardianFieldComp] {PawnOwner.LabelShort} has insufficient psyfocus ({PawnOwner.psychicEntropy?.CurrentPsyfocus ?? 0}) to recharge shield (cost: {Props.psyfocusCostPerInterval})."); + } + } + // The full recharge psyfocus deduction is removed, as it's now gradual. + // The wasNotAtFullHp check is still useful for other potential effects when full. + else if (wasNotAtFullHp && currentHitPoints >= HitPointsMax) + { + wasNotAtFullHp = false; // Reset flag when full + } + } + + // Projectile interception logic remains the same, as it's a separate Harmony patch + public bool TryIntercept(Projectile projectile) + { + if (!Active) return false; + + // We now intercept all projectiles, the filter will be done by the PostPreApplyDamage on self + // and the TryAbsorbDamageForAllyOnly for allies. + // This method is only for projectile visual/sound and psyfocus cost. + + // --- Interception Success --- + lastInterceptTicks = Find.TickManager.TicksGame; + + // Spawn effect at the point of interception, not the shield center + Props.interceptEffecter?.Spawn(projectile.ExactPosition.ToIntVec3(), PawnOwner.Map).Cleanup(); + + // No longer consuming hitpoints here, PostPreApplyDamage will handle it for self. + // The Harmony_DamageWorker for allies will handle it for allies. + + return true; + } + + // --- NEW: PostPreApplyDamage for self-protection (all damage types) --- + public override void PostPreApplyDamage(ref DamageInfo dinfo, out bool absorbed) + { + absorbed = false; + if (!Active || PawnOwner == null) return; // Only intercept if shield is active and for the owner + + // We intercept ALL damage types for the owner + if (currentHitPoints < dinfo.Amount) return; // Not enough HP to absorb + + // --- Absorption Success for self --- + Props.absorbEffecter?.Spawn(PawnOwner.Position, PawnOwner.Map).Cleanup(); // Effect at center for self-damage + + // Add entropy based on damage taken for self + if (PawnOwner.psychicEntropy != null && Props.entropyGainPerDamage > 0) + { + PawnOwner.psychicEntropy.TryAddEntropy(dinfo.Amount * Props.entropyGainPerDamage, overLimit: true); + } + + // Consume Hitpoints + currentHitPoints -= (int)dinfo.Amount; + if (currentHitPoints <= 0) + { + Break(); + } + absorbed = true; // Damage was absorbed + } + + + // --- REMOVED: Method for allies, called by Harmony patch --- + // public bool TryAbsorbDamageForAllyOnly(DamageInfo dinfo, Pawn allyPawn) + // { + // Log.Message($"[GuardianFieldComp] TryAbsorbDamageForAllyOnly for {allyPawn.LabelShort} (target) by {PawnOwner.LabelShort} (caster)."); + // Log.Message($" - transferAllyDamage: {Props.transferAllyDamage}"); + // Log.Message($" - Active: {Active}"); + // Log.Message($" - allyPawn == PawnOwner: {allyPawn == PawnOwner}"); + // Log.Message($" - allyPawn.Faction: {allyPawn.Faction?.Name ?? "Null"}, PawnOwner.Faction: {PawnOwner.Faction?.Name ?? "Null"}, FactionMatch: {allyPawn.Faction == PawnOwner.Faction}"); + // Log.Message($" - InRange: {Vector3.Distance(allyPawn.TrueCenter(), PawnOwner.TrueCenter()) <= Props.radius}"); + // Log.Message($" - HasHP: {currentHitPoints >= dinfo.Amount}"); + + // // Check if ally damage transfer is enabled + // if (!Props.transferAllyDamage) { Log.Message(" -> transferAllyDamage is false."); return false; } + + // // Shield must be active + // if (!Active) { Log.Message(" -> Shield is not Active."); return false; } + + // // Cannot absorb damage for self (handled by PostPreApplyDamage) + // if (allyPawn == PawnOwner) { Log.Message(" -> Target is self."); return false; } + + // // Only protect friendly pawns (same faction) + // if (allyPawn.Faction == null || PawnOwner.Faction == null || allyPawn.Faction != PawnOwner.Faction) { Log.Message(" -> Faction mismatch or null faction."); return false; } + + // // Target must be in range + // if (Vector3.Distance(allyPawn.TrueCenter(), PawnOwner.TrueCenter()) > Props.radius) { Log.Message(" -> Target out of range."); return false; } + + // // Check if shield has enough HP + // if (currentHitPoints < dinfo.Amount) { Log.Message(" -> Not enough HP to absorb damage."); return false; } + + // // --- Absorption Success for ally --- + // Props.absorbEffecter?.Spawn(allyPawn.Position, allyPawn.Map).Cleanup(); + + // // Add entropy based on damage taken for ally + // if (PawnOwner.psychicEntropy != null && Props.entropyGainPerAllyDamage > 0) + // { + // PawnOwner.psychicEntropy.TryAddEntropy(dinfo.Amount * Props.entropyGainPerAllyDamage, overLimit: true); + // } + + // // Consume Hitpoints + // currentHitPoints -= (int)dinfo.Amount; + // if (currentHitPoints <= 0) + // { + // Break(); + // } + + // Log.Message(" -> Damage absorption successful!"); + // return true; // Damage was absorbed + // } + + private void Break() + { + Props.breakEffecter?.Spawn(PawnOwner.Position, PawnOwner.Map).Cleanup(); + ticksToReset = Props.rechargeDelay; + currentHitPoints = 0; + } + + private void Reset() + { + if (PawnOwner.Spawned) + { + Props.reactivateEffecter?.Spawn(PawnOwner.Position, PawnOwner.Map).Cleanup(); + } + currentHitPoints = HitPointsMax; + } + + public override void PostDraw() + { + base.PostDraw(); + if (!Active || PawnOwner.Map == null) return; + + Vector3 drawPos = PawnOwner.Drawer.DrawPos; + drawPos.y = AltitudeLayer.MoteOverhead.AltitudeFor(); + + float alpha = GetCurrentAlpha(); + if (alpha > 0f) + { + Color color = Props.color; + color.a *= alpha; + MatPropertyBlock.SetColor(ShaderPropertyIDs.Color, color); + Matrix4x4 matrix = default; + matrix.SetTRS(drawPos, Quaternion.identity, new Vector3(Props.radius * 2f, 1f, Props.radius * 2f)); + Graphics.DrawMesh(MeshPool.plane10, matrix, ForceFieldMat, 0, null, 0, MatPropertyBlock); + } + } + + private float GetCurrentAlpha() + { + float idleAlpha = Mathf.Lerp(0.3f, 0.6f, (Mathf.Sin((float)parent.thingIDNumber + Time.realtimeSinceStartup * 1.5f) + 1f) / 2f); + float interceptAlpha = Mathf.Clamp01(1f - (float)(Find.TickManager.TicksGame - lastInterceptTicks) / 40f); + return Mathf.Max(idleAlpha, interceptAlpha); + } + + // --- GIZMO --- + public override System.Collections.Generic.IEnumerable CompGetGizmosExtra() + { + if (PawnOwner != null && Find.Selector.SingleSelectedThing == PawnOwner) + { + yield return new Gizmo_GuardianShieldStatus { shield = this }; + } + } + } + + // Gizmo class copied from the user's example and adapted + [StaticConstructorOnStartup] + public class Gizmo_GuardianShieldStatus : Gizmo + { + public ThingComp_GuardianPsyField shield; + private static readonly Texture2D FullShieldBarTex = SolidColorMaterials.NewSolidColorMaterial(new Color(0.3f, 0.8f, 0.8f), ShaderDatabase.MetaOverlay).mainTexture as Texture2D; + private static readonly Texture2D EmptyShieldBarTex = SolidColorMaterials.NewSolidColorMaterial(new Color(0.2f, 0.2f, 0.2f), ShaderDatabase.MetaOverlay).mainTexture as Texture2D; + + public override float GetWidth(float maxWidth) => 140f; + + public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms) + { + Rect rect = new Rect(topLeft.x, topLeft.y, GetWidth(maxWidth), 75f); + Rect rect2 = rect.ContractedBy(6f); + Widgets.DrawWindowBackground(rect); + + Rect labelRect = rect2; + labelRect.height = rect.height / 2f; + Text.Font = GameFont.Tiny; + Widgets.Label(labelRect, "Guardian Field"); + + Rect barRect = rect2; + barRect.yMin = rect2.y + rect2.height / 2f; + float fillPercent = (float)shield.currentHitPoints / shield.HitPointsMax; + Widgets.FillableBar(barRect, fillPercent, FullShieldBarTex, EmptyShieldBarTex, false); + + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.MiddleCenter; + + TaggedString statusText = shield.IsOnCooldown ? "Cooldown" : new TaggedString(shield.currentHitPoints + " / " + shield.HitPointsMax); + Widgets.Label(barRect, statusText); + + Text.Anchor = TextAnchor.UpperLeft; + + return new GizmoResult(GizmoState.Clear); + } + } +} \ No newline at end of file