Aetheria/Assets/Scripts/Gameplay/Weapons/Mine.cs
Meta Vens 95d6fd3b14 F4/F5: shield restore survives inactivity, and CanTakeHit stops mutating
F4: a hit that breaks the shield usually also cooks it (TakeHit's own
AddHeat), which can knock the item thermally offline in the same tick. The
reserve's recharge and its Broken-clearing check lived inside Execute, which
Entity.Update only runs while Item.Active is true -- so a shield broken by a
hit that also shuts it down could never come back, no matter how long
RestoreDuration allows for. That logic now lives in Shield.Update
(IAlwaysUpdatedBehavior), which Entity.Update calls every tick unconditionally
(Cooldown.cs already relies on the same interface for the same reason).

F5: CanTakeHit broke the shield and drained the reserve as a side effect of
the query itself, on the premise that every caller queries it exactly once
per hit. That premise was false: RaycastAll/SphereCastAll/OverlapSphere hand
every caller under Assets/Scripts/Gameplay/Weapons both the shield collider
and the hull collider for one shot, so the same shield's CanTakeHit was
queried twice per hit. CanTakeHit is now a pure read (Broken + the reserve's
own CanSpend); the mutation moved to a new Shield.Break(), idempotent, called
by each weapon file exactly where it already decided to route a hit past the
shield instead of absorbing it.

Pinned in tests/Aetheria.Shared.Tests/ShieldReserveTests.cs:
RestorationProceedsWhileTheItemIsInactive (F4),
CanTakeHitNeverMutatesRegardlessOfHowManyTimesItIsCalled and
BreakIsIdempotentAcrossRepeatedCalls (F5). Updated every existing test that
relied on CanTakeHit's old mutating side effect to call Break() explicitly,
matching the new caller pattern.

Cannot compile-verify Assets/Scripts/Gameplay/Weapons/*.cs here (UnityEngine,
Unity batchmode unavailable in this environment) -- reviewed by hand for
brace/control-flow correctness; Self or a Unity batchmode pass should confirm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:32:29 +02:00

123 lines
4.4 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using CultMath;
using static CultMath.math;
using UnityEngine;
using Random = UnityEngine.Random;
public class Mine : MonoBehaviour
{
public GridObject GridObject;
public MeshRenderer MeshRenderer;
public int EmissionSubmesh;
public AnimationCurve EmissionCurve;
public float ActiveCycleDuration;
public float ArmedCycleDuration;
public float ActiveEmission;
public float ArmedEmission;
public Prototype HitEffect;
public float Lifetime;
public float ActivationDelay;
public float BlastRange;
public float BlastDelay;
private float _startTime;
private float _blastTime;
private bool _blastCountdown;
private bool _active;
private Material _material;
private float _pulseLerp;
private float _emission;
public float Damage { get; set; }
public DamageType DamageType { get; set; }
public EntityInstance Source { get; set; }
public float Range { get; set; }
private void Start()
{
_material = MeshRenderer.materials[EmissionSubmesh];
}
private void OnEnable()
{
_startTime = Time.time;
_blastCountdown = false;
_active = false;
_emission = 0;
}
// Update is called once per frame
void Update()
{
var position = transform.position;
if (Time.time - _startTime > ActivationDelay && !_active)
{
_active = true;
}
if(_active)
{
_emission = lerp(_emission, _blastCountdown ? ArmedEmission : ActiveEmission, Time.deltaTime * 10);
_pulseLerp += Time.deltaTime / (_blastCountdown ? ArmedCycleDuration : ActiveCycleDuration);
_pulseLerp %= 1;
if(!_blastCountdown)
{
foreach (var collider in Physics.OverlapSphere(position, BlastRange, 1))
{
var hull = collider.GetComponent<HullCollider>();
if (hull)
{
_blastCountdown = true;
_blastTime = Time.time + BlastDelay;
}
}
}
if ((position - Source.transform.position).magnitude > Range ||
Time.time - _startTime > Lifetime ||
_blastCountdown && Time.time > _blastTime)
{
Explode();
}
}
_material.SetFloat("_Emission", _emission * EmissionCurve.Evaluate(_pulseLerp));
}
public void Explode()
{
var position = transform.position;
foreach (var collider in Physics.OverlapSphere(position, BlastRange, 1 | (1 << 17)))
{
var shield = collider.GetComponent<ShieldManager>();
if (shield)
{
var shieldBehavior = shield.Entity.Shield;
var shieldActive = shieldBehavior != null && shieldBehavior.Item.Active.Value;
var shieldAbsorbs = shieldActive && shieldBehavior.CanTakeHit(DamageType, Damage);
// F5 (docs/stats-and-power-cut.md, Soul pass 2026-09-19): CanTakeHit is a pure query now -- read
// once into shieldAbsorbs, and break the shield exactly once, right here, at the point this hit
// is actually decided to route past it (to the hull collider OverlapSphere returns as a
// separate entry in this same loop) instead of being absorbed. The hull branch below re-queries
// CanTakeHit safely -- it is side-effect-free -- and will see Broken already true.
if (shieldActive && !shieldAbsorbs) shieldBehavior.Break();
if (shieldAbsorbs)
{
shieldBehavior.TakeHit(DamageType, Damage);
shield.ShowHit(position, sqrt(Damage));
}
}
var hull = collider.GetComponent<HullCollider>();
if (hull && !(hull.Entity.Shield != null && hull.Entity.Shield.Item.Active.Value && hull.Entity.Shield.CanTakeHit(DamageType, Damage)))
{
hull.SendSplash(Damage, DamageType, Source.Entity, (collider.transform.position - position).normalized);
}
}
var ht = HitEffect.Instantiate<Transform>();
ht.position = position;
GetComponent<Prototype>().ReturnToPool();
}
}