Aetheria/Assets/Scripts/ServerShared/Agents/Agent.cs
soul 2ad52e7e9e PilotView: brains reach other ships only through their side's view, by opaque handles
One view per side over its members' sensors and books. Flight, Combat, Follow and Minion read contacts as ContactId
and fire-control readouts as view calls; FollowTask.Anchor is a handle. Own-ship reads stay direct.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:48:02 +02:00

131 lines
5.2 KiB
C#

/* 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 System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using CultMath;
using static CultMath.math;
using float2 = CultMath.float2;
public class Agent
{
private const float FORWARD_DELTA_THRESHOLD = 20;
private const float THRUST_DELTA_THRESHOLD = 1;
// The Strength of one Track paint in this ship's own IncomingPaints: a fire-control emitter tracking this
// that hard (rulings lock-warning-strength, sensor-stat-set item 4, launcher-lock-solution-quality).
private const float LockAlarm = .5f;
protected BaseState _rootState;
private BaseState _currentState;
private VelocityLimit _velocityLimit;
public AgentTask Task { get; set; }
// What this pilot's side knows of other ships. A ship outside a flight is a side of one: a view over itself, until
// Flight.Join sets the flight's.
public PilotView View { get; set; }
// Zone-scoped, from 1: the ordinal of this pilot's own dice, set by Zone.CreateAgent as it admits the pilot (0 for
// an agent built outside a zone's admission).
public uint Id { get; set; }
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, RoleDoctrine doctrine)
{
Ship = ship;
View = new PilotView(ship);
Doctrine = doctrine;
ItemManager = Ship.ItemManager;
Settings = Ship.ItemManager.GameplaySettings;
_rootState = _currentState = new BaseState(this);
_velocityLimit = Ship.GetBehavior<VelocityLimit>();
}
public void Transition(BaseState targetState, Action onTransition = null)
{
//ItemManager.Log($"{Ship.Name} changing state from {_currentState.GetType().Name} to {targetState.GetType().Name}");
_currentState.OnExitState();
_currentState = targetState;
onTransition?.Invoke();
_currentState.OnEnterState();
}
public void Update(float delta)
{
// The pilot tick commands Turn every tick, in every state: a state that pilots overwrites this zero with its
// demand, and one that does not leaves the hull holding course instead of inheriting the last state's rate.
Ship.Command.Turn = TurnCommand.OfRate(0);
VentOnTrack();
_currentState.Update(delta);
foreach (var transition in _currentState.Transitions)
if (transition.Condition())
{
Transition(transition.TargetState, transition.OnTransition);
break;
}
}
// The AI plays the player's rule on the player's information: vent when one Track paint on this ship passes half. It
// reads only this ship's own IncomingPaints, so it runs in every state (ruling vent-in-any-state), and a ship
// carrying no vent does nothing.
private void VentOnTrack()
{
if (TrackAlarm) Use(b => b is VapourDumpData);
}
// One Track paint on this ship passes the alarm: the ship vents, and does not overdrive on the same tick.
public bool TrackAlarm => Ship.IncomingPaints.Any(p => p.Mode == PaintMode.Track && p.Strength > LockAlarm);
// Through TryActivateConsumable, one of a kind at a time: a Stackable design would otherwise be drained once per tick.
public void Use(Predicate<BehaviorData> kind)
{
if (Ship.HasActiveConsumable(kind)) return;
var consumable = Ship.FirstCarriedConsumable(kind);
if (consumable != null) Ship.Command.Use(consumable);
}
public void Accelerate(float2 targetVelocity, bool noTurn = false)
{
var deltaV = targetVelocity - Ship.Velocity;
var deltaVMag = length(deltaV);
var deltaVDirection = normalize(deltaV);
// If Delta V is above the threshold, direct the ship towards the delta and use only main thrusters
if (!noTurn && deltaVMag > FORWARD_DELTA_THRESHOLD)
{
Ship.Command.Turn = TurnCommand.Toward(deltaVDirection);
Ship.Command.Movement = float2(0, pow(dot(Ship.Direction, deltaVDirection), 2));
}
// If Delta V is low, use all thrusters and leave Turn to the caller (MoveTo already pointed it at the target)
else if(deltaVMag > THRUST_DELTA_THRESHOLD)
{
var right = Ship.Direction.Rotate(ItemRotation.Clockwise);
Ship.Command.Movement = float2(dot(right, deltaVDirection), dot(Ship.Direction, deltaVDirection));
}
else
{
Ship.Command.Movement = float2.zero;
}
}
}
public class StateTransition
{
public StateTransition(BaseState targetState, Func<bool> condition, Action onTransition)
{
TargetState = targetState;
Condition = condition;
OnTransition = onTransition;
}
public BaseState TargetState { get; }
public Func<bool> Condition { get; }
public Action OnTransition { get; }
}