Selection is nothing the selected ship can sense: TargetedBy, TargetedByCount, their subscriptions and the music read of them are deleted. Indicator fills, the target panel, the compass, music and the enemy schematic rows now read CurrentEntity's ContactOf / RevealsOf / RevealedSubjects (new, the book's keys); the schematic rows are built by one builder from the book and rebuilt when the revealed set changes. Impact hull effect and projectile hit effects at another ship's position draw only through ActionGameManager.DrawsAt (the player's own ship or a Tracked contact). Builds untested in Unity at this commit; tests NothingReadsSelection and RevealedSubjectsListsTheBook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
4.9 KiB
C#
85 lines
4.9 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UniRx;
|
|
using CultMath;
|
|
using CultMath.UnityBridge;
|
|
using UnityEngine;
|
|
using static CultMath.math;
|
|
|
|
public class GuidedProjectileManager : InstantWeaponEffectManager
|
|
{
|
|
// Presentation follows the committed shot (FireControl): the round retargets to the burst point the commit
|
|
// names and bursts when the shot resolves, rather than predicting either from its own homing.
|
|
private static void Bind(GuidedProjectile p, Zone zone, int shotId)
|
|
{
|
|
Vector3 At(ShotOutcome o) => new Vector3(o.BurstPoint.x, p.transform.position.y, o.BurstPoint.y);
|
|
var bindings = new CompositeDisposable();
|
|
zone.ShotCommitted.Where(o => o.ShotId == shotId && o.HasBurstPoint).Subscribe(o => p.BurstAt(At(o))).AddTo(bindings);
|
|
zone.ShotResolved.Where(o => o.ShotId == shotId)
|
|
.Subscribe(o => p.Resolve(o.Result == ShotResult.Hit || o.Result == ShotResult.Burst, o.HasBurstPoint ? At(o) : (Vector3?) null,
|
|
ActionGameManager.DrawsAt(o.Target)))
|
|
.AddTo(bindings);
|
|
// The binding outlives a fade-out until the shot's own arrival (the moment the simulation resolves it), plus a
|
|
// second of margin; a shot no longer pending has nothing left to resolve.
|
|
p.BindingLifetime = zone.TryGetShot(shotId, out var shot) ? max(0f, shot.ArrivalTime - zone.Time) + 1f : 0f;
|
|
p.Binding = bindings; // replaces, and so disposes, any binding the projectile already had
|
|
}
|
|
|
|
public Prototype ProjectilePrototype;
|
|
|
|
public Subject<(Entity source, Transform target, GuidedProjectile missile)> OnFireGuided = new Subject<(Entity source, Transform target, GuidedProjectile missile)>();
|
|
|
|
public override void Fire(InstantWeapon weapon, EquippedItem item, EntityInstance source, EntityInstance target, int shotId)
|
|
{
|
|
if(weapon.Data is LauncherData launcher)
|
|
{
|
|
// A round the simulation fired at no target (FireControl.Fire: a fused round with no valid targeting
|
|
// data, or out of arc) flies to the point it bursts at; one with no target and no burst is not a shot.
|
|
PendingShot shot = default;
|
|
if (target == null && !(source.Entity.Zone.TryGetShot(shotId, out shot) && shot.Fuse != null)) return;
|
|
var burst = shot.BurstPosition.ToUnity();
|
|
var p = ProjectilePrototype.Instantiate<GuidedProjectile>();
|
|
p.ShotId = shotId;
|
|
p.Source = source.transform;
|
|
p.SourceEntity = source.Entity;
|
|
if (target != null) p.Target = target.transform;
|
|
else p.TargetPosition = () => burst;
|
|
p.Frequency = item.Evaluate(MunitionParts.Of<MunitionControlData>(item).EvasionFrequency);
|
|
var hp = item.Hardpoint;
|
|
var barrel = source.GetBarrel(hp);
|
|
p.StartPosition = (p.transform.position = barrel.position).ToCultMath();
|
|
p.Range = weapon.Range;
|
|
p.GuidanceCurve = launcher.GuidanceCurve.ToCurve();
|
|
p.LiftCurve = launcher.LiftCurve.ToCurve();
|
|
p.ThrustCurve = launcher.ThrustCurve.ToCurve();
|
|
p.Velocity = barrel.forward * weapon.Velocity;
|
|
p.Thrust = item.Evaluate(MunitionParts.Of<MunitionThrusterData>(item).Acceleration);
|
|
p.TopSpeed = item.Evaluate(MunitionParts.Of<MunitionThrusterData>(item).MaxSpeed);
|
|
Bind(p, source.Entity.Zone, shotId);
|
|
// A round with no target (a fused round fired at none) has no transform to hand a listener.
|
|
OnFireGuided.OnNext((source.Entity, target != null ? target.transform : null, p));
|
|
}
|
|
else if(weapon.Data is GuidedWeaponData guidance)
|
|
{
|
|
var p = ProjectilePrototype.Instantiate<GuidedProjectile>();
|
|
p.ShotId = shotId;
|
|
p.Source = source.transform;
|
|
p.SourceEntity = source.Entity;
|
|
p.Frequency = item.Evaluate(MunitionParts.Of<MunitionControlData>(item).EvasionFrequency);
|
|
var hp = item.Hardpoint;
|
|
var barrel = source.GetBarrel(hp);
|
|
p.StartPosition = (p.transform.position = barrel.position).ToCultMath();
|
|
p.Range = weapon.Range;
|
|
p.GuidanceCurve = guidance.GuidanceCurve.ToCurve();
|
|
p.LiftCurve = guidance.LiftCurve.ToCurve();
|
|
p.ThrustCurve = guidance.ThrustCurve.ToCurve();
|
|
p.Velocity = barrel.forward * weapon.Velocity;
|
|
p.Thrust = item.Evaluate(MunitionParts.Of<MunitionThrusterData>(item).Acceleration);
|
|
p.TopSpeed = item.Evaluate(MunitionParts.Of<MunitionThrusterData>(item).MaxSpeed);
|
|
p.TargetPosition = () => (source.Entity.Position + length(source.LookAtPoint.position.ToCultMath() - source.Entity.Position) * source.Entity.Aim).ToUnity();
|
|
Bind(p, source.Entity.Zone, shotId);
|
|
}
|
|
else Debug.LogError($"Weapon {item.Data.Name} linked to {name} effect, but is not a Launcher!");
|
|
}
|
|
}
|