/* 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; internal 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(); } 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 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 condition, Action onTransition) { TargetState = targetState; Condition = condition; OnTransition = onTransition; } public BaseState TargetState { get; } public Func Condition { get; } public Action OnTransition { get; } }