320 lines
14 KiB
C#
320 lines
14 KiB
C#
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);
|
|
}
|
|
}
|
|
} |