Faction play 1: typed doctrine on Faction and one flight per faction per zone
FactionDoctrine/RoleDoctrine (Faction key 17), Flight as the only writer of members' Target and Task, hail/grace/compliance, FollowTask/FollowState, agent removal on Entities removal, doctrine-catalog command, FlightTests. Not yet built or run; verification follows in later commits. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4d664b537f
commit
8f42265de3
24 changed files with 812 additions and 42 deletions
|
|
@ -23,11 +23,14 @@ public class Agent
|
|||
public Ship Ship { get; }
|
||||
public ItemManager ItemManager { get; }
|
||||
public GameplaySettings Settings { get; }
|
||||
// How this pilot fights: its faction's doctrine for its role (Zone.CreateAgent resolves it).
|
||||
public RoleDoctrine Doctrine { get; }
|
||||
public float TopSpeed => _velocityLimit?.Limit ?? 100;
|
||||
|
||||
public Agent(Ship ship)
|
||||
public Agent(Ship ship, RoleDoctrine doctrine)
|
||||
{
|
||||
Ship = ship;
|
||||
Doctrine = doctrine;
|
||||
ItemManager = Ship.ItemManager;
|
||||
Settings = Ship.ItemManager.GameplaySettings;
|
||||
|
||||
|
|
|
|||
178
Assets/Scripts/ServerShared/Agents/Flight.cs
Normal file
178
Assets/Scripts/ServerShared/Agents/Flight.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
// One flight per (zone, faction) of piloted ships; unaffiliated ships form one flight of their own. The flight alone
|
||||
// decides what its members do: which hostile each engages (Ship.Target) and what each does otherwise (Agent.Task).
|
||||
// The agent graph executes those two and decides nothing. The faction's doctrine (FactionDoctrine) holds the numbers.
|
||||
//
|
||||
// The picture is the union of the members' VisibleEnemies. A track passes through phases the flight owns, runtime only:
|
||||
// None, Hailing (grace running, one hail spoken), Complied (slow enough at the grace's end: followed, not engaged) and
|
||||
// Engaged. A member still fires only at what its own VisibleEntities hold (FireControl), so the shared picture never
|
||||
// lets it fire at what it cannot see.
|
||||
public class Flight
|
||||
{
|
||||
public enum Phase { None, Hailing, Complied, Engaged }
|
||||
|
||||
private class Track
|
||||
{
|
||||
public Phase Phase;
|
||||
public double Start;
|
||||
}
|
||||
|
||||
private class Member
|
||||
{
|
||||
public Agent Agent;
|
||||
public PatrolOrbitsTask Patrol;
|
||||
}
|
||||
|
||||
private readonly FactionDoctrine _doctrine;
|
||||
private readonly GameplaySettings _settings;
|
||||
private readonly List<Member> _members = new List<Member>();
|
||||
private readonly Dictionary<Entity, Track> _tracks = new Dictionary<Entity, Track>();
|
||||
private double _time;
|
||||
|
||||
// No doctrine fights as FactionDoctrine.Default; a doctrine that leaves a role unauthored gets that role's default.
|
||||
public Flight(FactionDoctrine doctrine, GameplaySettings settings)
|
||||
{
|
||||
var fallback = FactionDoctrine.Default(settings);
|
||||
_doctrine = doctrine ?? fallback;
|
||||
_settings = settings;
|
||||
Combatant = _doctrine.Combatant ?? fallback.Combatant;
|
||||
}
|
||||
|
||||
public RoleDoctrine Combatant { get; }
|
||||
public int Count => _members.Count;
|
||||
public bool Contains(Agent agent) => _members.Any(m => m.Agent == agent);
|
||||
|
||||
public Phase PhaseOf(Entity track) => _tracks.TryGetValue(track, out var t) ? t.Phase : Phase.None;
|
||||
|
||||
public void Join(Agent agent, PatrolOrbitsTask circuit)
|
||||
{
|
||||
_members.Add(new Member { Agent = agent, Patrol = circuit });
|
||||
agent.Task = circuit;
|
||||
}
|
||||
|
||||
public void Leave(Agent agent) => _members.RemoveAll(m => m.Agent == agent);
|
||||
|
||||
public void Update(float dt)
|
||||
{
|
||||
_time += dt;
|
||||
var picture = _members.SelectMany(m => m.Agent.Ship.VisibleEnemies).Distinct().ToList();
|
||||
foreach (var gone in _tracks.Keys.Where(t => !picture.Contains(t)).ToList()) _tracks.Remove(gone);
|
||||
|
||||
foreach (var entity in picture)
|
||||
{
|
||||
if (!_tracks.TryGetValue(entity, out var track)) _tracks[entity] = track = new Track();
|
||||
Advance(entity, track);
|
||||
}
|
||||
|
||||
foreach (var member in _members) Order(member, picture);
|
||||
}
|
||||
|
||||
// A grudge is a hostile IFF override that any member holds on the track.
|
||||
private bool Grudged(Entity entity) => _members.Any(m => m.Agent.Ship.IffOverride(entity) == true);
|
||||
|
||||
private bool Engageable(Entity entity, bool grudge)
|
||||
{
|
||||
switch (_doctrine.EngageOn)
|
||||
{
|
||||
case EngageOn.Never: return false;
|
||||
case EngageOn.Detection: return true;
|
||||
case EngageOn.Identified:
|
||||
return grudge || _members.Any(m => m.Agent.Ship.EntityInfoGathered.TryGetValue(entity, out var info) &&
|
||||
info >= _settings.TargetGearInfoThreshold);
|
||||
case EngageOn.Provoked: return grudge;
|
||||
case EngageOn.Trespass: return grudge || !(entity.PresencePermitted?.Value ?? true);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Advance(Entity entity, Track track)
|
||||
{
|
||||
if (track.Phase == Phase.Engaged) return;
|
||||
var grudge = Grudged(entity);
|
||||
if (!Engageable(entity, grudge))
|
||||
{
|
||||
track.Phase = Phase.None;
|
||||
return;
|
||||
}
|
||||
|
||||
// A grudge is already a quarrel: no grace to give.
|
||||
if (grudge)
|
||||
{
|
||||
track.Phase = Phase.Engaged;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (track.Phase)
|
||||
{
|
||||
case Phase.None:
|
||||
if (_doctrine.Grace > 0)
|
||||
{
|
||||
track.Phase = Phase.Hailing;
|
||||
track.Start = _time;
|
||||
if (!string.IsNullOrEmpty(_doctrine.Hail)) Nearest(entity)?.Agent.Ship.SetMessage(_doctrine.Hail);
|
||||
}
|
||||
else track.Phase = Phase.Engaged;
|
||||
break;
|
||||
case Phase.Hailing:
|
||||
if (_time >= track.Start + _doctrine.Grace)
|
||||
track.Phase = _doctrine.ComplySpeed > 0 && length(entity.Velocity) < _doctrine.ComplySpeed
|
||||
? Phase.Complied
|
||||
: Phase.Engaged;
|
||||
break;
|
||||
case Phase.Complied:
|
||||
if (length(entity.Velocity) >= _doctrine.ComplySpeed) track.Phase = Phase.Engaged;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Member Nearest(Entity entity) =>
|
||||
_members.OrderBy(m => lengthsq(m.Agent.Ship.Position.xz - entity.Position.xz)).FirstOrDefault();
|
||||
|
||||
private void Order(Member member, List<Entity> picture)
|
||||
{
|
||||
var ship = member.Agent.Ship;
|
||||
var seen = ship.VisibleEnemies;
|
||||
|
||||
// Engage: keep the current target while it stays engaged, else the nearest engaged track this member can see.
|
||||
var engaged = picture.Where(e => _tracks[e].Phase == Phase.Engaged && seen.Contains(e)).ToList();
|
||||
if (engaged.Count > 0)
|
||||
{
|
||||
var current = ship.Target.Value.Entity;
|
||||
var pick = current != null && engaged.Contains(current)
|
||||
? current
|
||||
: engaged.OrderBy(e => lengthsq(ship.Position.xz - e.Position.xz)).First();
|
||||
Aim(ship, pick);
|
||||
return;
|
||||
}
|
||||
|
||||
Aim(ship, null);
|
||||
var held = picture.Where(e => _tracks[e].Phase == Phase.Hailing || _tracks[e].Phase == Phase.Complied).ToList();
|
||||
if (held.Count > 0)
|
||||
{
|
||||
var anchor = member.Agent.Task is FollowTask follow && held.Contains(follow.Anchor)
|
||||
? follow.Anchor
|
||||
: held.OrderBy(e => lengthsq(ship.Position.xz - e.Position.xz)).First();
|
||||
if (!(member.Agent.Task is FollowTask current) || current.Anchor != anchor)
|
||||
member.Agent.Task = new FollowTask { Anchor = anchor, Standoff = Standoff(ship) };
|
||||
}
|
||||
else if (member.Agent.Task != member.Patrol)
|
||||
member.Agent.Task = member.Patrol;
|
||||
}
|
||||
|
||||
private static void Aim(Ship ship, Entity target)
|
||||
{
|
||||
var wanted = target == null ? TargetRef.None : new TargetRef(target);
|
||||
if (!ship.Target.Value.Equals(wanted)) ship.SetTarget(wanted);
|
||||
}
|
||||
|
||||
private float Standoff(Ship ship)
|
||||
{
|
||||
if (Combatant.HoldStandoff > 0) return Combatant.HoldStandoff;
|
||||
return 1.1f * ship.Weapons.Select(w => w.Range).DefaultIfEmpty(0f).Max();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ServerShared/Agents/Flight.cs.meta
Normal file
2
Assets/Scripts/ServerShared/Agents/Flight.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5f6c82a74e5b4bc1b8c79e0d242e3ec2
|
||||
|
|
@ -1,23 +1,30 @@
|
|||
|
||||
using System.Linq;
|
||||
using UniRx;
|
||||
|
||||
// The agent graph for a piloted ship. It executes the Task and Target its flight writes (Flight.cs) and decides
|
||||
// neither: patrol and follow follow the Task kind, combat follows the Target.
|
||||
public class Minion : Agent
|
||||
{
|
||||
public Minion(Ship ship) : base(ship)
|
||||
public Minion(Ship ship, RoleDoctrine doctrine) : base(ship, doctrine)
|
||||
{
|
||||
var patrolState = new PatrolOrbitsState(this);
|
||||
_rootState.AddTransition(patrolState,
|
||||
() => Task is PatrolOrbitsTask,
|
||||
() => patrolState.Task = Task as PatrolOrbitsTask);
|
||||
var followState = new FollowState(this);
|
||||
void Patrol() => patrolState.Task = Task as PatrolOrbitsTask;
|
||||
_rootState.AddTransition(patrolState, () => Task is PatrolOrbitsTask, Patrol);
|
||||
_rootState.AddTransition(followState, () => Task is FollowTask);
|
||||
|
||||
Ship.VisibleEnemies.ObserveAdd().Where(_ => Ship.Target.Value.IsNone).Subscribe(add => Ship.SetTarget(add.Value));
|
||||
// A Task change switches the substate in one update, from wherever the pilot is in the old task. includeChildren
|
||||
// reaches the sub-states (MoveToOrbit) that Update actually sits in.
|
||||
patrolState.AddTransition(followState, () => Task is FollowTask, null, true, _rootState);
|
||||
followState.AddTransition(patrolState, () => Task is PatrolOrbitsTask, Patrol, true, _rootState);
|
||||
patrolState.AddTransition(_rootState, () => !(Task is PatrolOrbitsTask), null, true, _rootState);
|
||||
followState.AddTransition(_rootState, () => !(Task is FollowTask), null, true, _rootState);
|
||||
|
||||
var combatState = new CombatState(this);
|
||||
_rootState.AddTransition(combatState,
|
||||
() => Ship.Target.Value.Entity != null, null, true, _rootState);
|
||||
|
||||
|
||||
combatState.AddTransition(_rootState,
|
||||
() => Ship.Target.Value.Entity == null, null, true, _rootState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public class CombatState : BaseState
|
|||
{
|
||||
private const int DPS_SAMPLE_COUNT = 32;
|
||||
private float _optimumRange;
|
||||
public float OptimumRange => _optimumRange;
|
||||
private readonly List<(int index, float dps)> _availableGroups = new List<(int index, float dps)>();
|
||||
private readonly List<LockWeapon> _availableLockingWeapons = new List<LockWeapon>();
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ public class CombatState : BaseState
|
|||
// fires at, and "the target is designated" for a fused one.
|
||||
foreach (var weapon in _agent.Ship.WeaponGroups[selectedGroup].weapons)
|
||||
{
|
||||
if (FireControl.AgentFires(weapon, _agent.Ship, target))
|
||||
if (FireControl.AgentFires(weapon, _agent.Ship, target, _agent.Doctrine.MinHitProbability))
|
||||
weapon.Activate();
|
||||
else if (weapon.Firing)
|
||||
weapon.Deactivate();
|
||||
|
|
@ -156,7 +157,7 @@ public class CombatState : BaseState
|
|||
}
|
||||
|
||||
// Multiply by range raised to a (sublinear) exponent to bias DPS preference towards longer ranges
|
||||
dps *= pow(range, _agent.Settings.AgentRangeExponent);
|
||||
dps *= pow(range, _agent.Doctrine.RangeExponent);
|
||||
|
||||
if (dps > optimumDPS)
|
||||
{
|
||||
|
|
|
|||
26
Assets/Scripts/ServerShared/Agents/States/Follow.cs
Normal file
26
Assets/Scripts/ServerShared/Agents/States/Follow.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using CultMath;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
|
||||
// Hold a standoff from the anchor: match its velocity, and close or open the gap in proportion to the error.
|
||||
public class FollowState : BaseState
|
||||
{
|
||||
public FollowState(Agent agent) : base(agent) { }
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
var task = _agent.Task as FollowTask;
|
||||
if (task?.Anchor == null) return;
|
||||
|
||||
var toAnchor = task.Anchor.Position.xz - _agent.Ship.Position.xz;
|
||||
var distance = length(toAnchor);
|
||||
var direction = distance > 0 ? toAnchor / distance : _agent.Ship.Direction;
|
||||
// Positive error: too far, close. Negative: too near, open.
|
||||
var error = distance - task.Standoff;
|
||||
var pull = direction * (float) sign(error) * saturate(abs(error) / max(task.Standoff, 1f));
|
||||
var desired = task.Anchor.Velocity + pull * _agent.TopSpeed;
|
||||
|
||||
_agent.Ship.Aim = float3(direction.x, 0, direction.y);
|
||||
_agent.Accelerate(desired);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ServerShared/Agents/States/Follow.cs.meta
Normal file
2
Assets/Scripts/ServerShared/Agents/States/Follow.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5cf86244ca6648a99f304b061b66348a
|
||||
|
|
@ -29,6 +29,7 @@ public enum TaskType
|
|||
Tow,
|
||||
Defend,
|
||||
Attack,
|
||||
Explore
|
||||
Explore,
|
||||
Follow
|
||||
}
|
||||
|
||||
|
|
|
|||
11
Assets/Scripts/ServerShared/Agents/Tasks/FollowTask.cs
Normal file
11
Assets/Scripts/ServerShared/Agents/Tasks/FollowTask.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
using MessagePack;
|
||||
|
||||
// Keep station on an entity at a standoff: the flight hailing a track holds its members here (Flight.cs).
|
||||
// Runtime only, like every agent task.
|
||||
public class FollowTask : AgentTask
|
||||
{
|
||||
[IgnoreMember] public Entity Anchor;
|
||||
[IgnoreMember] public float Standoff;
|
||||
[IgnoreMember] public override TaskType Type => TaskType.Follow;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6bab02326ff64e29ad2b81f0fc1483b5
|
||||
|
|
@ -80,7 +80,7 @@ public class TurretController : Behavior, IInitializableBehavior
|
|||
{
|
||||
// The same per-weapon decision Combat.cs makes; the range test is part of it (a shot out of range
|
||||
// is not designated and prices at zero).
|
||||
if (FireControl.AgentFires(x, Entity, target))
|
||||
if (FireControl.AgentFires(x, Entity, target, Entity.ItemManager.GameplaySettings.AgentMinHitProbability))
|
||||
{
|
||||
x.Activate();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@ public class Faction
|
|||
|
||||
[Inspectable, JsonProperty("bossMusic"), Key(15)]
|
||||
public uint BossMusic;
|
||||
|
||||
// Key 16 is reserved for Relations (operator ruling faction-relations-field).
|
||||
|
||||
// The faction's rules of engagement; null fights as FactionDoctrine.Default.
|
||||
[Inspectable, JsonProperty("doctrine"), Key(17)]
|
||||
public FactionDoctrine Doctrine;
|
||||
}
|
||||
|
||||
[CultDocument("aetheria.namefile", "1"), Inspectable, MessagePackObject]
|
||||
|
|
|
|||
74
Assets/Scripts/ServerShared/Doctrine.cs
Normal file
74
Assets/Scripts/ServerShared/Doctrine.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
// What lets a flight open fire. The flight reads it per track; nothing else does.
|
||||
public enum EngageOn
|
||||
{
|
||||
// Any hostile the flight's members can see.
|
||||
Detection,
|
||||
// A hostile whose gear one member has resolved (EntityInfoGathered at or past TargetGearInfoThreshold).
|
||||
Identified,
|
||||
// Only a hostile that a member holds a hostile IFF override on (a grudge).
|
||||
Provoked,
|
||||
// A hostile whose presence this faction's zone does not permit.
|
||||
Trespass,
|
||||
// Never, not even for a grudge.
|
||||
Never
|
||||
}
|
||||
|
||||
// A faction's rules of engagement (operator ruling faction-doctrine-typed): flight-level fields plus one RoleDoctrine
|
||||
// per role. Keys 5-8 are reserved for faction-play-2, keys 9-10 for faction-play-3. A faction with no doctrine gets
|
||||
// Default, which fights as the agents did before doctrine existed.
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class FactionDoctrine
|
||||
{
|
||||
[Inspectable, JsonProperty("engageOn"), Key(0)]
|
||||
public EngageOn EngageOn;
|
||||
|
||||
// Seconds between a flight hailing a track and opening fire on it. Zero fires at once, with no hail.
|
||||
[Inspectable, JsonProperty("grace"), Key(1)]
|
||||
public float Grace;
|
||||
|
||||
[Inspectable, JsonProperty("hail"), Key(2)]
|
||||
public string Hail;
|
||||
|
||||
// A hailed track slower than this at the end of the grace counts as complying and is followed, not engaged.
|
||||
// Zero means nothing complies.
|
||||
[Inspectable, JsonProperty("complySpeed"), Key(3)]
|
||||
public float ComplySpeed;
|
||||
|
||||
[Inspectable, JsonProperty("combatant"), Key(4)]
|
||||
public RoleDoctrine Combatant;
|
||||
|
||||
public static FactionDoctrine Default(GameplaySettings settings) => new FactionDoctrine
|
||||
{
|
||||
EngageOn = EngageOn.Detection,
|
||||
Combatant = new RoleDoctrine
|
||||
{
|
||||
RangeExponent = settings.AgentRangeExponent,
|
||||
MinHitProbability = settings.AgentMinHitProbability
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// How one role fights. Combatant is the only role so far.
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class RoleDoctrine
|
||||
{
|
||||
// Bias of the preferred engagement range towards longer ranges (CombatState's optimum range).
|
||||
[Inspectable, JsonProperty("rangeExponent"), Key(0)]
|
||||
public float RangeExponent;
|
||||
|
||||
// A weapon fires only when its hit probability reaches this.
|
||||
[Inspectable, JsonProperty("minHitProbability"), Key(1)]
|
||||
public float MinHitProbability;
|
||||
|
||||
// Distance a flight holds from a track it is hailing or following. Zero means 1.1 x the member's longest weapon range.
|
||||
[Inspectable, JsonProperty("holdStandoff"), Key(2)]
|
||||
public float HoldStandoff;
|
||||
}
|
||||
2
Assets/Scripts/ServerShared/Doctrine.cs.meta
Normal file
2
Assets/Scripts/ServerShared/Doctrine.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3f9a1c5e7b2d4e68a0c4d1b9e7f25a31
|
||||
|
|
@ -814,6 +814,9 @@ public abstract class Entity
|
|||
else _iffOverrides.Remove(other);
|
||||
}
|
||||
|
||||
// The override this entity holds on another, or null when it holds none (the derived rule decides).
|
||||
public bool? IffOverride(Entity other) => _iffOverrides.TryGetValue(other, out var hostile) ? hostile : (bool?) null;
|
||||
|
||||
public bool IsHostileTo(Entity other, bool recursive = false)
|
||||
{
|
||||
// An override decides only the stance of the entity that holds it. The recursive=true calls
|
||||
|
|
|
|||
|
|
@ -101,12 +101,13 @@ public static class FireControl
|
|||
// target is designated, whatever the arc: the round is sent out of arc on purpose (Solve). An agent never
|
||||
// fires a fused weapon at nothing -- no target, or one it holds no valid data on, is no reason to fish -- and
|
||||
// never one Solve refuses: a refused weapon is not a weapon it can use.
|
||||
public static bool AgentFires(Weapon weapon, Entity shooter, Entity target)
|
||||
// The hit-probability threshold is the shooter's doctrine (RoleDoctrine.MinHitProbability), or the turret's global.
|
||||
public static bool AgentFires(Weapon weapon, Entity shooter, Entity target, float minHitProbability)
|
||||
{
|
||||
// A mine layer lays only at a designated target in range, never at nothing and never on a hit probability.
|
||||
if (IsMineLayer(weapon)) return Solve(weapon, weapon.Item, shooter, target).Designated;
|
||||
if (FuseOf(weapon.Item, out _) == null)
|
||||
return HitProbability(weapon, shooter, target) >= shooter.ItemManager.GameplaySettings.AgentMinHitProbability;
|
||||
return HitProbability(weapon, shooter, target) >= minHitProbability;
|
||||
var solution = Solve(weapon, weapon.Item, shooter, target);
|
||||
return solution.Designated && solution.Outcome != FireOutcome.Refused;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ public class Zone
|
|||
private ItemManager _itemManager;
|
||||
private double _time;
|
||||
public List<Agent> Agents = new List<Agent>();
|
||||
// One flight per faction, created on its first piloted join; unaffiliated ships share Unaffiliated. A flight owns
|
||||
// what its members target and do (Agents/Flight.cs).
|
||||
private static readonly Faction Unaffiliated = new Faction();
|
||||
private readonly Dictionary<Faction, Flight> _flights = new Dictionary<Faction, Flight>();
|
||||
public IEnumerable<Flight> Flights => _flights.Values;
|
||||
|
||||
// Cut 2 (docs/mining-cut.md): chunk wear, keyed by chunk. Zone.Wear is the only writer; ChunkExists and
|
||||
// ChunkRadius are the only readers besides the pack/unpack round trip below.
|
||||
|
|
@ -106,6 +111,16 @@ public class Zone
|
|||
// ObserveAdd fires for all of them. Forbidden writer: no presentation may remove an entity from Zone.
|
||||
Entities.ObserveAdd().Subscribe(add => add.Value.Death.Subscribe(_ => { Entities.Remove(add.Value); add.Value.Deactivate(); }));
|
||||
|
||||
// Leaving Entities, by death, docking or a transfer, is leaving the zone's pilots: the agent and its flight
|
||||
// membership go with the ship, so nothing keeps flying a hull that is not here.
|
||||
Entities.ObserveRemove().Subscribe(remove =>
|
||||
{
|
||||
var agent = Agents.FirstOrDefault(a => a.Ship == remove.Value);
|
||||
if (agent == null) return;
|
||||
Agents.Remove(agent);
|
||||
foreach (var flight in _flights.Values) flight.Leave(agent);
|
||||
});
|
||||
|
||||
Targets.Add(new EntityTargets());
|
||||
|
||||
foreach (var orbit in pack.Orbits)
|
||||
|
|
@ -173,10 +188,13 @@ public class Zone
|
|||
|
||||
private Agent CreateAgent(Ship ship)
|
||||
{
|
||||
var agent = new Minion(ship);
|
||||
var key = ship.Faction ?? Unaffiliated;
|
||||
if (!_flights.TryGetValue(key, out var flight))
|
||||
_flights[key] = flight = new Flight(ship.Faction?.Doctrine, _itemManager.GameplaySettings);
|
||||
var agent = new Minion(ship, flight.Combatant);
|
||||
var task = new PatrolOrbitsTask();
|
||||
task.Circuit = Orbits.OrderBy(_ => _itemManager.Random.NextFloat()).Take(4).Select(x => x.Key).ToArray();
|
||||
agent.Task = task;
|
||||
flight.Join(agent, task);
|
||||
return agent;
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +236,8 @@ public class Zone
|
|||
orbit.Value.Velocity = (orbit.Value.Position - orbit.Value.PreviousPosition) / deltaTime;
|
||||
}
|
||||
|
||||
foreach(var agent in Agents)
|
||||
foreach (var flight in _flights.Values) flight.Update(deltaTime);
|
||||
foreach(var agent in Agents.ToArray())
|
||||
agent.Update(deltaTime);
|
||||
|
||||
foreach (var entity in Entities.ToArray()) entity.Update(deltaTime);
|
||||
|
|
|
|||
Loading…
Reference in a new issue