Ruling vent-in-any-state. The vent decision (own Track paint above .5, one of a kind through TryActivateConsumable) moves out of CombatState into Agent.Update, which every state runs; CombatState's overdrive asks Agent.TrackAlarm so a venting tick still does not overdrive. ConsumableAiTests: AVentDoesNotNeedAnyEnemyInSight is master's premise again (empty VisibleEnemies), a patrolling agent has its own test, and the fixture zone has one orbit so an unengaged pilot patrols it. Not built or run yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
4.8 KiB
C#
124 lines
4.8 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; }
|
|
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 writes 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.Turn = 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.TryActivateConsumable(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.Turn = Steering.Toward(Ship, deltaVDirection);
|
|
Ship.MovementDirection = 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.MovementDirection = float2(dot(right, deltaVDirection), dot(Ship.Direction, deltaVDirection));
|
|
}
|
|
else
|
|
{
|
|
Ship.MovementDirection = 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; }
|
|
}
|
|
|