Aetheria/Assets/Scripts/ServerShared/Agents/Agent.cs
soul 9a2a9390e1 Command seam, agents: one PilotCommand per entity, applied once per step by PilotApplier in Zone.Update
Agent, Flight, Combat, MoveTo and Follow write only Ship.Command. Entities
with no agent apply first, then each agent applies right after it decides.
A pilot reads its own pending designation and aim item back from its command;
FireControl gains Refuses/AgentFires/HitProbability overloads that take them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:32:11 +02:00

127 lines
5 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; }
// 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;
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; }
}