From 446f84458cd45b77c34a071e4f319a6e88416874 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 20:57:56 +0200 Subject: [PATCH 1/4] Pin the feedback-1 cue path's remaining mutants (follow-up feedback-1-test-gaps) A direct absorb's heading and damage type are the shot's, on both layers (non-Kinetic weapon, both heading components negative); a blast absorb carries the blast's damage type and the target's height on both layers; DrawAhead.Impact sits at the hull's height. Tests only; every commit builds. Co-Authored-By: Claude Opus 5.5 --- .../ImpactFeedbackTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs b/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs index 49c34202a..a72837edd 100644 --- a/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs +++ b/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs @@ -323,6 +323,51 @@ public sealed partial class FireControlCut124Tests Assert.Equal(DamageType.Kinetic, shield.DamageType); } + // The direct absorb's direction and damage type are the shot's: a non-Kinetic weapon fired along a heading with + // both components negative, so neither an abs of the direction nor a constant Kinetic type can agree with it. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ADirectAbsorbCarriesTheShotsHeadingAndDamageTypeWhateverTheyAre(bool shielded) + { + var e = Build(TestSettings(), SolidShape(21, 3), velocity: 0, damage: 100f, equipShield: shielded, shieldCapacity: 100000f, custom: new[] { ("Marker", new Shape(), new int2(2, 1), 1f) }); + e.Weapon.WeaponData.DamageType = DamageType.Thermal; + e.Target.Position = e.Shooter.Position + float3(-30f, 0f, -40f); + for (var i = 0; i < 30; i++) e.Zone.Update(.1f); + var subs = new List(); + var (outcome, absorbs) = FireAtTheMarker(e, subs); + foreach (var s in subs) s.Dispose(); + + Assert.Equal(shielded, outcome.Shielded); + var absorb = Assert.Single(absorbs); + Assert.Equal(shielded ? AbsorbLayer.Shield : AbsorbLayer.Hull, absorb.Layer); + Assert.Equal(DamageType.Thermal, absorb.DamageType); + Assert.Equal(-.6f, absorb.Direction.x, 1e-3); + Assert.Equal(0f, absorb.Direction.y, 1e-6); + Assert.Equal(-.8f, absorb.Direction.z, 1e-3); + } + + [Fact] + public void ABlastAbsorbCarriesTheBlastsDamageTypeAndTheTargetsHeightOnBothLayers() + { + foreach (var shield in new[] { false, true }) + { + var e = Build(TestSettings(), SolidShape(5, 4), equipShield: shield, shieldCapacity: 100000f); + e.HullData.GridOffset = 5f; + for (var i = 0; i < 30; i++) e.Zone.Update(.1f); + Assert.Equal(5f, e.Target.Position.y, 1e-4); + var subs = new List(); + var takes = Collect(e.Target, subs); + FireControl.Detonate(e.Zone, e.Target.Position.xz + float2(1f, 0f), 2f, 50f, DamageType.Thermal); + foreach (var s in subs) s.Dispose(); + + var take = Assert.Single(takes); + Assert.Equal(shield ? AbsorbLayer.Shield : AbsorbLayer.Hull, take.Layer); + Assert.Equal(DamageType.Thermal, take.DamageType); + Assert.Equal(5f, take.Position.y, 1e-4); + } + } + [Fact] public void ABlastShieldAbsorbCarriesTheShareTheBlastPointAndTheAwayDirection() { @@ -404,6 +449,9 @@ public sealed partial class FireControlCut124Tests var point = float3(t.Position.x + 2f, 9f, t.Position.z + 1f); Assert.Equal(9f, DrawAhead.Carried(t, point, 0f).y, 1e-4); Assert.Equal(DrawAhead.Position(t, .5f).y + 4f, DrawAhead.Carried(t, point, .5f).y, 1e-4); + + // The drawn impact cell sits at the hull's own height, not at world y 0. + Assert.Equal(DrawAhead.Position(t, .5f).y, DrawAhead.Impact(t, new int2(7, 1), .5f).y, 1e-4); } } From 789fe9fcdcc7d67d3d639f55418639f7051b3764 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 21:09:13 +0200 Subject: [PATCH 2/4] Turn the shooter to the heading the direct-absorb test fires along The mount fires inside an arc about the shooter's facing; the first version fired backwards and found no hit. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs b/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs index a72837edd..203a5347a 100644 --- a/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs +++ b/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs @@ -332,6 +332,8 @@ public sealed partial class FireControlCut124Tests { var e = Build(TestSettings(), SolidShape(21, 3), velocity: 0, damage: 100f, equipShield: shielded, shieldCapacity: 100000f, custom: new[] { ("Marker", new Shape(), new int2(2, 1), 1f) }); e.Weapon.WeaponData.DamageType = DamageType.Thermal; + // The mount fires inside an arc about the shooter's facing, so the shooter turns to the heading it fires along. + e.Shooter.Direction = float2(-.6f, -.8f); e.Target.Position = e.Shooter.Position + float3(-30f, 0f, -40f); for (var i = 0; i < 30; i++) e.Zone.Update(.1f); var subs = new List(); From 97d94ecfae26e1a06a4b8ee7ef5e9cd7b88347c0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 21:10:04 +0200 Subject: [PATCH 3/4] Present landed impacts: shield ripple, hull reaction and camera kick through one ImpactPresenter ImpactPresenter (bound on every EntityInstance by SetEntity through the one CapabilityPresentationBinder) renders feedback-1's absorbs: Feedback.Cue decides intensity and shake, DrawAhead.Carried the drawn point; a hull absorb spawns ImpactFeedback's pooled effect under the instance, and a cue with shake raises a Cinemachine impulse. ShieldManager ripples on a Shield-layer absorb. MainMenu gains a Screen Shake slider that persists on Back. Editor: WireImpactFeedback (scene wiring, scene diff in the next commit) and ImpactPresenterSmoke. Unity batchmode compile 0 error CS; ImpactPresenterSmoke exit 0. The scene is not yet wired in this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Editor/ImpactPresenterSmoke.cs | 134 ++++++++++++++++++ .../Editor/ImpactPresenterSmoke.cs.meta | 2 + Assets/Scripts/Editor/WireImpactFeedback.cs | 61 ++++++++ .../Scripts/Editor/WireImpactFeedback.cs.meta | 2 + Assets/Scripts/Gameplay/EntityInstance.cs | 7 + Assets/Scripts/Gameplay/ImpactFeedback.cs | 39 +++++ .../Scripts/Gameplay/ImpactFeedback.cs.meta | 2 + Assets/Scripts/Gameplay/ImpactPresenter.cs | 47 ++++++ .../Scripts/Gameplay/ImpactPresenter.cs.meta | 2 + Assets/Scripts/Gameplay/ShieldManager.cs | 10 +- Assets/Scripts/UI/MainMenu.cs | 5 + 11 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 Assets/Scripts/Editor/ImpactPresenterSmoke.cs create mode 100644 Assets/Scripts/Editor/ImpactPresenterSmoke.cs.meta create mode 100644 Assets/Scripts/Editor/WireImpactFeedback.cs create mode 100644 Assets/Scripts/Editor/WireImpactFeedback.cs.meta create mode 100644 Assets/Scripts/Gameplay/ImpactFeedback.cs create mode 100644 Assets/Scripts/Gameplay/ImpactFeedback.cs.meta create mode 100644 Assets/Scripts/Gameplay/ImpactPresenter.cs create mode 100644 Assets/Scripts/Gameplay/ImpactPresenter.cs.meta diff --git a/Assets/Scripts/Editor/ImpactPresenterSmoke.cs b/Assets/Scripts/Editor/ImpactPresenterSmoke.cs new file mode 100644 index 000000000..42dc9d56d --- /dev/null +++ b/Assets/Scripts/Editor/ImpactPresenterSmoke.cs @@ -0,0 +1,134 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using GameCult.Caching; +using UnityEditor; +using UnityEngine; +using static CultMath.math; + +// Batch proof of ImpactPresenter's wiring, without a scene: a zone ship is bound to an EntityInstance through the game's +// own SetEntity, and impacts are published on the ship's Capabilities exactly as FireControl does. A hull absorb spawns +// one hull effect under the instance and, for the player's ship at ScreenShake 1, raises one camera impulse; ScreenShake 0 +// or another ship raises none; a shield absorb spawns no hull effect. The shield ripple itself (ShieldManager.ShowHit +// needs a shield material and a rig) is looked at in the editor, not here. +// +// Unity -batchmode -nographics -projectPath -executeMethod ImpactPresenterSmoke.Run -logFile +// +// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure. +public static class ImpactPresenterSmoke +{ + private const BindingFlags Any = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + + public static void Run() + { + var work = Path.Combine(Path.GetTempPath(), $"impact-presenter-smoke-{Guid.NewGuid():N}"); + GameObject[] objects = null; + try + { + Directory.CreateDirectory(work); + objects = Check(work); + Clean(objects, work); + EditorApplication.Exit(0); + } + catch (Exception exception) + { + Debug.LogException(exception); + Clean(objects, work); + EditorApplication.Exit(1); + } + } + + // Edit mode runs no Awake or Start; the smoke runs the ones the game's scene would have. + private static void Awaken(Component component) => component.GetType().GetMethod("Awake", Any)?.Invoke(component, null); + + private static GameObject[] Check(string work) + { + var settings = AssetDatabase.LoadAssetAtPath("Assets/Resources/Settings.asset")?.GameplaySettings + ?? throw new InvalidOperationException("No Settings.asset."); + + var shape = new Shape(5, 4); + foreach (var cell in shape.AllCoordinates) shape[cell] = true; + var hullData = new HullData + { + Name = "Smoke Hull", HullType = HullType.Ship, Shape = shape, Durability = 1000, Mass = 1000, Armor = 0 + }; + var cache = AetheriaStores.Open(Path.Combine(work, "smoke.cc"), catalogWritable: true); + cache.Upsert(new VerseGrammar { Revision = 1 }); + cache.Upsert(hullData); + cache.FlushAsync().Wait(); + var ledger = new ProvenanceLedger(); + for (var i = 1; i <= 8; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 }; + var items = new ItemManager(cache, ledger, settings, _ => { }); + var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Smoke", Owner = null }, null); + var hullRef = items.ItemData.RefOf(items.ItemData.GetByName("Smoke Hull")); + var ship = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 1 }, new EntitySettings()); + var other = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 2 }, new EntitySettings()); + zone.Entities.Add(ship); + zone.Entities.Add(other); + ship.Activate(); + other.Activate(); + + // The scene's side: the tuning object with a pooled effect, and the game manager that names the player's ship. + var feedbackObject = new GameObject("Smoke Impact Feedback"); + var feedback = feedbackObject.AddComponent(); + Awaken(feedback); + var effectObject = new GameObject("Smoke Hull Effect"); + feedback.HullEffect = effectObject.AddComponent(); + var managerObject = new GameObject("Smoke Game Manager"); + var manager = managerObject.AddComponent(); + typeof(ActionGameManager).GetProperty("Instance", Any).SetValue(null, manager); + manager.CurrentEntity = ship; + + var instanceObject = new GameObject("Smoke Entity Instance"); + var instance = instanceObject.AddComponent(); + Awaken(instance); + instance.HullColliders = new HullCollider[0]; + instance.RadiatorHardpoints = new RadiatorHardpoint[0]; + instance.ThrusterHardpoints = new ThrusterHardpoint[0]; + instance.WeaponHardpoints = new WeaponHardpoint[0]; + instance.ArticulationPoints = new ArticulationPoint[0]; + instance.SetEntity(null, ship); + + int Effects() => instance.GetComponentsInChildren(true).Count(p => p != null && !p.isOriginalPrototype); + void Expect(string what, int effects, int kicks) + { + if (Effects() != effects || feedback.KicksRaised != kicks) + throw new InvalidOperationException($"{what}: {Effects()} hull effects (want {effects}), {feedback.KicksRaised} impulses (want {kicks})."); + } + AbsorbEvent Hit(AbsorbLayer layer) => new AbsorbEvent(float3(1f, 0f, 2f), float3(0f, 0f, 1f), 100f, DamageType.Kinetic, layer); + + ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 1f; + ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull)); + Expect("player's hull hit", 1, 1); + var effect = instance.GetComponentsInChildren(true).Single(p => !p.isOriginalPrototype); + var drawn = DrawAhead.Carried(ship, float3(1f, 0f, 2f), ActionGameManager.Clock.Lead); + if (abs(effect.transform.position.x - drawn.x) > 1e-3f || abs(effect.transform.position.z - drawn.z) > 1e-3f) + throw new InvalidOperationException("The hull effect is not at the drawn impact point."); + + ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 0f; + ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull)); + Expect("hull hit at ScreenShake 0", 2, 1); + + ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 1f; + manager.CurrentEntity = other; + ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull)); + Expect("hull hit on another ship", 3, 1); + + manager.CurrentEntity = ship; + ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Shield)); + Expect("player's shield hit", 3, 2); + + Debug.Log("IMPACT_PRESENTER_SMOKE ok effects=3 impulses=2"); + return new[] { feedbackObject, effectObject, managerObject, instanceObject }; + } + + private static void Clean(GameObject[] objects, string work) + { + if (objects != null) + foreach (var go in objects) + if (go != null) UnityEngine.Object.DestroyImmediate(go); + try { if (Directory.Exists(work)) Directory.Delete(work, true); } + catch (Exception error) { Debug.LogWarning($"Impact presenter smoke left {work}: {error.Message}"); } + } +} diff --git a/Assets/Scripts/Editor/ImpactPresenterSmoke.cs.meta b/Assets/Scripts/Editor/ImpactPresenterSmoke.cs.meta new file mode 100644 index 000000000..05df104cd --- /dev/null +++ b/Assets/Scripts/Editor/ImpactPresenterSmoke.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b1b27e4493b2cfe48805c786a5090cd3 \ No newline at end of file diff --git a/Assets/Scripts/Editor/WireImpactFeedback.cs b/Assets/Scripts/Editor/WireImpactFeedback.cs new file mode 100644 index 000000000..650db6981 --- /dev/null +++ b/Assets/Scripts/Editor/WireImpactFeedback.cs @@ -0,0 +1,61 @@ +using System; +using Cinemachine; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +// Adds the impact-feedback tuning object to ARPG.unity and the impulse listener to its follow camera, then saves the +// scene. Idempotent: a scene that already has them is left as it is. +// +// Unity -batchmode -projectPath -executeMethod WireImpactFeedback.Run -logFile +// +// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure. The menu item runs the same wiring. +public static class WireImpactFeedback +{ + private const string ScenePath = "Assets/Scenes/ARPG.unity"; + // The weapons' own hit effect (the nested prefab AC5's Projectile uses as HitEffect): no new art. + private const string HullEffectPath = "Assets/Prefabs/Fire & Explosion Effects/Prefabs/TinyExplosion.prefab"; + + [MenuItem("Aetheria/Wire Impact Feedback")] + public static void Menu() => Wire(); + + public static void Run() + { + try + { + Wire(); + EditorApplication.Exit(0); + } + catch (Exception exception) + { + Debug.LogException(exception); + EditorApplication.Exit(1); + } + } + + private static void Wire() + { + var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single); + var manager = UnityEngine.Object.FindFirstObjectByType(); + if (manager == null || manager.FollowCamera == null) throw new InvalidOperationException("ARPG.unity has no ActionGameManager with a FollowCamera."); + + if (manager.FollowCamera.GetComponent() == null) + manager.FollowCamera.gameObject.AddComponent(); + + if (UnityEngine.Object.FindFirstObjectByType() == null) + { + var prefab = AssetDatabase.LoadAssetAtPath(HullEffectPath); + if (prefab == null) throw new InvalidOperationException($"No prefab at {HullEffectPath}."); + var root = new GameObject("Impact Feedback"); + var feedback = root.AddComponent(); + var effect = (GameObject) PrefabUtility.InstantiatePrefab(prefab, root.transform); + feedback.HullEffect = effect.GetComponent(); + if (feedback.HullEffect == null) throw new InvalidOperationException("The hull effect prefab has no Prototype."); + EditorUtility.SetDirty(feedback); + } + + EditorSceneManager.MarkSceneDirty(scene); + if (!EditorSceneManager.SaveScene(scene)) throw new InvalidOperationException("The scene did not save."); + Debug.Log("WIRE_IMPACT_FEEDBACK ok"); + } +} diff --git a/Assets/Scripts/Editor/WireImpactFeedback.cs.meta b/Assets/Scripts/Editor/WireImpactFeedback.cs.meta new file mode 100644 index 000000000..5d106d375 --- /dev/null +++ b/Assets/Scripts/Editor/WireImpactFeedback.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9083abd01a947a847847d2f6fc177153 \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/EntityInstance.cs b/Assets/Scripts/Gameplay/EntityInstance.cs index 53e5dbc71..56617ab5a 100644 --- a/Assets/Scripts/Gameplay/EntityInstance.cs +++ b/Assets/Scripts/Gameplay/EntityInstance.cs @@ -167,6 +167,13 @@ public class EntityInstance : MonoBehaviour if(Shield) Shield.Entity = entity; + + // Landed impacts reach the screen through the entity's one binding: the shield ripple (ShieldManager) and the + // hull reaction and camera kick (ImpactPresenter) subscribe to the Absorb stream the simulation publishes. + var impacts = gameObject.GetComponent() ?? gameObject.AddComponent(); + impacts.Entity = entity; + (gameObject.GetComponent() ?? gameObject.AddComponent()) + .Bind(entity.Capabilities); foreach (var hullCollider in HullColliders) hullCollider.Entity = entity; foreach (var item in entity.Equipment) diff --git a/Assets/Scripts/Gameplay/ImpactFeedback.cs b/Assets/Scripts/Gameplay/ImpactFeedback.cs new file mode 100644 index 000000000..6c1f43df1 --- /dev/null +++ b/Assets/Scripts/Gameplay/ImpactFeedback.cs @@ -0,0 +1,39 @@ +using Cinemachine; +using UnityEngine; + +// Presentation tuning for landed impacts (ImpactPresenter reads it, nothing in the simulation does): the hull +// reaction's effect prefab and size, how strongly a hit's share of the hull reads (Feedback.Cue's intensityScale), +// and the camera impulse. A scene component, so the scene is where it is tuned; absent in menus and test scenes, +// where ImpactPresenter does nothing. +public class ImpactFeedback : MonoBehaviour +{ + public static ImpactFeedback Instance { get; private set; } + + // A scene instance of a pooled hit effect (Prototype); a hit spawns one under the struck EntityInstance. + public Prototype HullEffect; + + // Multiplies a hit's share of the receiver's max durability before it saturates to 0..1 (Feedback.Cue). + public float IntensityScale = 10f; + + // The effect's scale never drops below this, so the lightest hit still reads. + public float MinEffectScale = .3f; + + public CinemachineImpulseDefinition Impulse = new CinemachineImpulseDefinition(); + + // Impulses raised since the scene loaded; the batchmode smoke reads it. + public int KicksRaised { get; private set; } + + private void Awake() => Instance = this; + + private void OnDestroy() + { + if (Instance == this) Instance = null; + } + + // Raises one camera impulse of the given force, travelling along `direction` from `position`. + public void Kick(Vector3 position, Vector3 direction, float force) + { + KicksRaised++; + Impulse.CreateEvent(position, direction.normalized * force); + } +} diff --git a/Assets/Scripts/Gameplay/ImpactFeedback.cs.meta b/Assets/Scripts/Gameplay/ImpactFeedback.cs.meta new file mode 100644 index 000000000..1d5613e95 --- /dev/null +++ b/Assets/Scripts/Gameplay/ImpactFeedback.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bfdc38a81d00b0347bb55ad6469535ff \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/ImpactPresenter.cs b/Assets/Scripts/Gameplay/ImpactPresenter.cs new file mode 100644 index 000000000..f8c68c6db --- /dev/null +++ b/Assets/Scripts/Gameplay/ImpactPresenter.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using CultMath.UnityBridge; +using UnityEngine; + +// How one entity's landed impacts look and feel: the hull reaction at the drawn impact point, and the player's camera +// kick. The simulation publishes each impact once on the entity's Capabilities (FireControl); Feedback.Cue decides how +// hard it reads and whether the camera feels it, DrawAhead.Carried decides where it is drawn; this decides nothing. +// The shield ripple is ShieldManager's. Everything here is display-only and runs in real time. +public class ImpactPresenter : MonoBehaviour, IAbsorbPresenter +{ + public Entity Entity; + + private readonly List _live = new List(); + + public void Absorb(AbsorbEvent e) + { + var feedback = ImpactFeedback.Instance; + if (feedback == null || Entity == null) return; + + var manager = ActionGameManager.Instance; + var cue = Feedback.Cue(e, Entity.EquippedHull.Data.Durability, + manager != null && Entity == manager.CurrentEntity, + ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake, feedback.IntensityScale); + var point = DrawAhead.Carried(Entity, cue.Position, ActionGameManager.Clock.Lead).ToUnity(); + + if (e.Layer == AbsorbLayer.Hull && feedback.HullEffect != null) + { + _live.RemoveAll(live => live == null || !live.gameObject.activeSelf); + var effect = feedback.HullEffect.Instantiate(); + var size = Mathf.Max(feedback.MinEffectScale, cue.Intensity); + effect.transform.SetParent(transform, false); + effect.transform.position = point; + effect.transform.localScale = Vector3.one * (size / Mathf.Max(transform.lossyScale.x, 1e-3f)); + _live.Add(effect); + } + + if (cue.Shake > 0f) + feedback.Kick(point, new Vector3(cue.Direction.x, 0f, cue.Direction.z), cue.Shake); + } + + // A live effect is a pooled child of this instance; hand it back before the instance takes it down with it. + private void OnDestroy() + { + foreach (var live in _live) + if (live != null && live.gameObject.activeSelf) live.ReturnToPool(); + } +} diff --git a/Assets/Scripts/Gameplay/ImpactPresenter.cs.meta b/Assets/Scripts/Gameplay/ImpactPresenter.cs.meta new file mode 100644 index 000000000..4c69429bc --- /dev/null +++ b/Assets/Scripts/Gameplay/ImpactPresenter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 775c61f785344d54b9f41ad3328cecee \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/ShieldManager.cs b/Assets/Scripts/Gameplay/ShieldManager.cs index 46d0b36a5..7e380c17b 100644 --- a/Assets/Scripts/Gameplay/ShieldManager.cs +++ b/Assets/Scripts/Gameplay/ShieldManager.cs @@ -8,7 +8,7 @@ using static CultMath.math; using UniRx; using UnityEngine; -public class ShieldManager : MonoBehaviour +public class ShieldManager : MonoBehaviour, IAbsorbPresenter { public Prototype ShieldPrototype; public float CollisionHitDuration = 3; @@ -86,6 +86,14 @@ public class ShieldManager : MonoBehaviour } } + // A shot the shield took ripples it where the shot landed, drawn on the hull as it is drawn. The hull layer is + // ImpactPresenter's. + public void Absorb(AbsorbEvent e) + { + if (e.Layer != AbsorbLayer.Shield || Entity == null || Entity.Shield == null || !Entity.Shield.Item.Active.Value) return; + ShowHit(DrawAhead.Carried(Entity, e.Position, ActionGameManager.Clock.Lead).ToUnity(), CollisionHitDuration); + } + public void ShowHit(Vector3 point, float duration) { var shield = ShieldPrototype.Instantiate(); diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index 7c2666420..bd019ab21 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -299,9 +299,14 @@ public class MainMenu : MonoBehaviour _nextMenu.panel.AddField("Show Asteroids in Minimap", () => ActionGameManager.PlayerSettings.GraphicsSettings.ShowAsteroidsInMinimap, b => ActionGameManager.PlayerSettings.GraphicsSettings.ShowAsteroidsInMinimap = b); + _nextMenu.panel.AddField("Screen Shake", + () => ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake, + f => ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = f, + 0, 1); _nextMenu.panel.AddButton("Back", () => { + ActionGameManager.SavePlayerSettings(); ShowSettings(); Fade(false); }); From 9afa5700a9e22452b530836ad579926c017e1c87 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 21:10:07 +0200 Subject: [PATCH 4/4] Wire ImpactFeedback into ARPG.unity (WireImpactFeedback output) Adds the Impact Feedback object (TinyExplosion, the weapons' own hit effect, as the pooled hull effect) and a CinemachineImpulseListener on the follow camera. Additions only (188 lines, 0 removed). Co-Authored-By: Claude Opus 5.5 --- Assets/Scenes/ARPG.unity | 188 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/Assets/Scenes/ARPG.unity b/Assets/Scenes/ARPG.unity index 20f43bb49..a11091e71 100644 --- a/Assets/Scenes/ARPG.unity +++ b/Assets/Scenes/ARPG.unity @@ -2205,6 +2205,81 @@ MonoBehaviour: m_FlexibleWidth: -1 m_FlexibleHeight: -1 m_LayoutPriority: 1 +--- !u!1001 &177891521 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 775333809} + m_Modifications: + - target: {fileID: 1337198075554910, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_Name + value: TinyExplosion + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalPosition.x + value: -4.2346153 + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalPosition.y + value: 2.42 + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalPosition.z + value: -21.91896 + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalRotation.w + value: 0.7071068 + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalRotation.x + value: -0.7071068 + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: -90 + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, type: 3} +--- !u!114 &177891522 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 8662091027736085435, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, + type: 3} + m_PrefabInstance: {fileID: 177891521} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1281fe44325f9b548aa42dc331b17266, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!4 &177891523 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 4441266138185142, guid: 25c7aa9ad705fe143a4d71c1c8deb7d4, + type: 3} + m_PrefabInstance: {fileID: 177891521} + m_PrefabAsset: {fileID: 0} --- !u!1 &178733679 GameObject: m_ObjectHideFlags: 0 @@ -12201,6 +12276,94 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 0} +--- !u!1 &775333807 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 775333809} + - component: {fileID: 775333808} + m_Layer: 0 + m_Name: Impact Feedback + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &775333808 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 775333807} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bfdc38a81d00b0347bb55ad6469535ff, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::ImpactFeedback + HullEffect: {fileID: 177891522} + IntensityScale: 10 + MinEffectScale: 0.3 + Impulse: + m_ImpulseChannel: 1 + m_ImpulseShape: 0 + m_CustomImpulseShape: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + m_ImpulseDuration: 0.2 + m_ImpulseType: 3 + m_DissipationRate: 0 + m_RawSignal: {fileID: 0} + m_AmplitudeGain: 1 + m_FrequencyGain: 1 + m_RepeatMode: 0 + m_Randomize: 1 + m_TimeEnvelope: + m_AttackShape: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + m_DecayShape: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + m_AttackTime: 0 + m_SustainTime: 0.2 + m_DecayTime: 0.7 + m_ScaleWithImpact: 1 + m_HoldForever: 0 + m_ImpactRadius: 100 + m_DirectionMode: 0 + m_DissipationMode: 2 + m_DissipationDistance: 100 + m_PropagationSpeed: 343 +--- !u!4 &775333809 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 775333807} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 177891523} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &776416423 GameObject: m_ObjectHideFlags: 0 @@ -13630,6 +13793,7 @@ GameObject: m_Component: - component: {fileID: 887677623} - component: {fileID: 887677622} + - component: {fileID: 887677624} m_Layer: 0 m_Name: Third Person Rig m_TagString: Untagged @@ -13692,6 +13856,29 @@ Transform: - {fileID: 1319157835} m_Father: {fileID: 1665127616} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &887677624 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 887677621} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 00b2d199b96b516448144ab30fb26aed, type: 3} + m_Name: + m_EditorClassIdentifier: Cinemachine::Cinemachine.CinemachineImpulseListener + m_ApplyAfter: 2 + m_ChannelMask: 1 + m_Gain: 1 + m_Use2DDistance: 0 + m_UseCameraSpace: 1 + m_ReactionSettings: + m_SecondaryNoise: {fileID: 0} + m_AmplitudeGain: 1 + m_FrequencyGain: 1 + m_Duration: 1 + m_NoiseOffsets: {x: 0, y: 0, z: 0} --- !u!1 &906767441 GameObject: m_ObjectHideFlags: 0 @@ -38310,3 +38497,4 @@ SceneRoots: - {fileID: 990038542} - {fileID: 2135526003} - {fileID: 495677244} + - {fileID: 775333809}