This commit is contained in:
Tourswen
2025-09-29 00:10:36 +08:00
8 changed files with 532 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<AbilityDef>
<defName>ARA_GuardianPsyField_On</defName>
<label>守护者灵能场</label>
<description>投射一个强大的灵能防御力场。</description>
<iconPath>ArachnaeSwarm/UI/Abilities/ARA_RaceBaseSwarmProduceOn</iconPath>
<cooldownTicksRange>601</cooldownTicksRange>
<hostile>false</hostile>
<showOnCharacterCard>true</showOnCharacterCard>
<aiCanUse>false</aiCanUse>
<verbProperties>
<verbClass>Verb_CastAbility</verbClass>
<warmupTime>0</warmupTime>
<drawAimPie>false</drawAimPie>
<requireLineOfSight>false</requireLineOfSight>
<targetable>false</targetable>
<targetParams>
<canTargetSelf>true</canTargetSelf>
</targetParams>
</verbProperties>
<comps>
<li Class="CompProperties_AbilityGiveHediff">
<compClass>ArachnaeSwarm.CompAbilityEffect_GiveSwitchHediff</compClass>
<hediffDef>ARA_GuardianPsyField</hediffDef>
<applyToSelf>true</applyToSelf>
</li>
</comps>
</AbilityDef>
<AbilityDef>
<defName>ARA_GuardianPsyField_Off</defName>
<label>守护者灵能场</label>
<description>关闭灵能防御力场。</description>
<iconPath>ArachnaeSwarm/UI/Abilities/ARA_RaceBaseSwarmProduceOff</iconPath>
<cooldownTicksRange>601</cooldownTicksRange>
<hostile>false</hostile>
<showOnCharacterCard>true</showOnCharacterCard>
<aiCanUse>false</aiCanUse>
<verbProperties>
<verbClass>Verb_CastAbility</verbClass>
<warmupTime>0</warmupTime>
<drawAimPie>false</drawAimPie>
<requireLineOfSight>false</requireLineOfSight>
<targetable>false</targetable>
<targetParams>
<canTargetSelf>true</canTargetSelf>
</targetParams>
</verbProperties>
<comps>
<li Class="CompProperties_AbilityRemoveHediff">
<compClass>ArachnaeSwarm.CompAbilityEffect_RemoveSwitchHediff</compClass>
<hediffDef>ARA_GuardianPsyField</hediffDef>
<applyToSelf>true</applyToSelf>
</li>
</comps>
</AbilityDef>
</Defs>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<HediffDef>
<defName>ARA_GuardianPsyField</defName>
<label>守护者灵能场</label>
<description>一个强大的灵能防御力场。它会自动拦截进入其作用范围的敌对飞行物,并能将附近友军受到的伤害转移到施法者身上。每次成功的守护都会消耗施法者的精神力。</description>
<hediffClass>ArachnaeSwarm.Hediff_DynamicInterceptor</hediffClass>
<isBad>false</isBad>
<defaultLabelColor>(0.6, 0.2, 0.9)</defaultLabelColor>
<scenarioCanAdd>false</scenarioCanAdd>
<comps>
<li Class="ArachnaeSwarm.HediffCompProperties_DynamicInterceptor">
<guardianProps Class="ArachnaeSwarm.CompProperties_GuardianPsyField">
<!-- Basic functionality -->
<radius>5.9</radius>
<hitPoints>1500</hitPoints>
<rechargeDelay>3200</rechargeDelay>
<rechargeHitPointsIntervalTicks>60</rechargeHitPointsIntervalTicks>
<!-- Psyfocus/Entropy Mechanics -->
<!-- Removed psyfocusCostForFullRecharge as it's now gradual -->
<psyfocusCostPerInterval>0.001</psyfocusCostPerInterval> <!-- e.g., 0.1% of max psyfocus per recharge interval -->
<entropyGainPerDamage>0.1</entropyGainPerDamage> <!-- 1 entropy per 2 damage -->
<hitPointsPctPerInterval>0.01</hitPointsPctPerInterval> <!-- Restore 1% of max HP per interval -->
<!-- Visuals and Sound -->
<color>(0.5, 0.3, 0.9, 0.5)</color>
<interceptEffecter>Interceptor_BlockedProjectile</interceptEffecter>
<absorbEffecter>Interceptor_BlockedProjectile</absorbEffecter> <!-- Reusing an effecter for now -->
<breakEffecter>Shield_Break</breakEffecter>
<reactivateEffecter>BulletShieldGenerator_Reactivate</reactivateEffecter>
</guardianProps>
</li>
</comps>
</HediffDef>
</Defs>

View File

@@ -291,6 +291,8 @@
<abilities>
<li>ARA_PsychicBrainburn</li>
<li>ARA_NeuroSwarm_jump</li>
<li>ARA_GuardianPsyField_On</li>
<li>ARA_GuardianPsyField_Off</li>
<!-- <li>ARA_Ability_Morph</li> -->
</abilities>
<apparelTags>

View File

@@ -169,6 +169,9 @@
<Compile Include="Hediffs\ProphecyGearEffect.cs" />
<Compile Include="Hediffs\WULA_HediffDamgeShield\DRMDamageShield.cs" />
<Compile Include="Hediffs\WULA_HediffDamgeShield\Hediff_DamageShield.cs" />
<Compile Include="Hediff_DynamicInterceptor.cs" />
<Compile Include="ThingComp_GuardianPsyField.cs" />
<Compile Include="Harmony_ProjectileInterceptor.cs" />
<Compile Include="MainHarmony.cs" />
<Compile Include="Thing_Comps\CompAndPatch_GiveHediffOnShot.cs" />
<Compile Include="Pawn_Comps\ARA_AutoMechCarrier\CompAutoMechCarrier.cs" />

View File

@@ -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<ThingComp_GuardianPsyField>(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;
}
}
}

View File

@@ -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<ThingComp_GuardianPsyField>() == 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<ThingComp_GuardianPsyField>();
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;
}
}

View File

@@ -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<Gizmo> 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);
}
}
}