Files
ArachnaeSwarm/Source/ArachnaeSwarm/ThingComp_GuardianPsyField.cs
2025-10-01 12:26:49 +08:00

257 lines
9.8 KiB
C#

using RimWorld;
using Verse;
using UnityEngine;
using Verse.Sound;
namespace ArachnaeSwarm
{
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
public float psyfocusCostPerInterval = 0.001f;
public float entropyGainPerDamage = 0.5f;
public float hitPointsPctPerInterval = 0.01f;
public EffecterDef absorbEffecter;
public bool interceptGroundProjectiles = true;
public bool interceptNonHostileProjectiles = false;
public bool interceptAirProjectiles = true;
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
{
private int lastInterceptTicks = -999999;
private int ticksToReset = 0;
public int currentHitPoints;
private bool wasNotAtFullHp = false;
public CompProperties_GuardianPsyField Props => (CompProperties_GuardianPsyField)props;
private Pawn PawnOwner => parent as Pawn;
public bool IsOnCooldown => ticksToReset > 0;
public int HitPointsMax => Props.hitPoints;
private static readonly Material ForceFieldMat = MaterialPool.MatFrom("Other/ForceField", ShaderDatabase.MoteGlow);
private static readonly MaterialPropertyBlock MatPropertyBlock = new MaterialPropertyBlock();
public bool Active
{
get
{
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;
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();
}
}
else if (Active && currentHitPoints < HitPointsMax)
{
if (PawnOwner.psychicEntropy != null && PawnOwner.psychicEntropy.CurrentPsyfocus >= Props.psyfocusCostPerInterval)
{
wasNotAtFullHp = true;
if(this.parent.IsHashIntervalTick(Props.rechargeHitPointsIntervalTicks))
{
currentHitPoints += (int)(HitPointsMax * Props.hitPointsPctPerInterval);
if(currentHitPoints > HitPointsMax) currentHitPoints = HitPointsMax;
PawnOwner.psychicEntropy.OffsetPsyfocusDirectly(-Props.psyfocusCostPerInterval);
}
}
}
else if (wasNotAtFullHp && currentHitPoints >= HitPointsMax)
{
wasNotAtFullHp = false;
}
}
// Helper method to apply costs
private void ApplyCosts(float damageAmount)
{
if (PawnOwner.psychicEntropy != null && Props.entropyGainPerDamage > 0)
{
PawnOwner.psychicEntropy.TryAddEntropy(damageAmount * Props.entropyGainPerDamage, overLimit: true);
}
currentHitPoints -= (int)damageAmount;
if (currentHitPoints <= 0)
{
Break();
}
}
public bool TryIntercept(Projectile projectile, Vector3 lastExactPos, Vector3 newExactPos)
{
if (!Active) return false;
if (currentHitPoints <= 0) return false;
if (!GenGeo.IntersectLineCircleOutline(PawnOwner.Position.ToVector2(), Props.radius, lastExactPos.ToVector2(), newExactPos.ToVector2()))
{
return false;
}
if (projectile.def.projectile.flyOverhead && !Props.interceptAirProjectiles) return false;
if (!projectile.def.projectile.flyOverhead && !Props.interceptGroundProjectiles) return false;
if (projectile.Launcher != null && !projectile.Launcher.HostileTo(PawnOwner.Faction) && !Props.interceptNonHostileProjectiles) return false;
lastInterceptTicks = Find.TickManager.TicksGame;
Props.interceptEffecter?.Spawn(projectile.ExactPosition.ToIntVec3(), PawnOwner.Map).Cleanup();
// Apply costs for intercepting projectile
ApplyCosts(projectile.DamageAmount);
return true;
}
public override void PostPreApplyDamage(ref DamageInfo dinfo, out bool absorbed)
{
absorbed = false;
if (!Active || PawnOwner == null) return;
// We only handle non-projectile damage here, as projectiles are handled by TryIntercept
if (dinfo.Def.isRanged) return;
if (currentHitPoints < dinfo.Amount) return;
Props.absorbEffecter?.Spawn(PawnOwner.Position, PawnOwner.Map).Cleanup();
// Apply costs for absorbing non-projectile damage
ApplyCosts(dinfo.Amount);
absorbed = true;
}
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);
}
public override System.Collections.Generic.IEnumerable<Gizmo> CompGetGizmosExtra()
{
if (PawnOwner != null && Find.Selector.SingleSelectedThing == PawnOwner)
{
yield return new Gizmo_GuardianShieldStatus { shield = this };
}
}
}
[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, "ARA_GuardianFieldGizmoLabel".Translate());
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);
}
}
}