ZeroEpsilon/src/engine/vessel.cr

253 lines
7.4 KiB
Crystal

require "./gravity"
class Vessel(N)
getter :name, :gravity_body, :gravity_field, :stations
getter :pilot, :navigation
def initialize(@name : String, @gravity_body : Gravity::MovingBody(N), @gravity_field : Gravity::Field(N))
@pilot = Pilot(N).new
@navigation = Navigation(N).new
@stations = {
pilot: @pilot,
navigation: @navigation,
}
# -- bottom after instances variables are set, we can reference self --
@stations.each { |sn, s| s.vessel = self }
end
def execute_orders!
@stations.each { |sn, s| s.execute_orders! }
end
def gravity : Vector(Float64, N)
@gravity_field.acceleration(@gravity_body.position)
end
abstract class Station(N)
getter orders
setter vessel
def initialize(@vessel : Vessel(N)? = nil)
@orders = Hash(Symbol, Array(Order(N))).new
end
def vessel?
@vessel
end
def vessel : Vessel(N)
vessel = @vessel
raise "@vessel cannot be Nil" if vessel.nil?
vessel
end
# like a orders[group] but will never throw and always
# have an initialized array.
# It is not a default value because it can modify the orders state.
def orders_at(group : Symbol)
@orders[group] ||= Array(Order(N)).new
@orders[group]
end
def orders_at(order : Order(N))
orders_at(order.group)
end
def <<(order : Order(N))
order.station = self
orders_at(order.group) << order
end
def order(priority : Int32 = 0, group : Symbol = :default, &block : Order(N) -> Nil)
self << Order(N).new(priority: priority, group: group, station: self, &block)
end
# Given one `Order`, this method remove all next orders in the list
# so we don't execute them next tick.
def clear_next!(order : Order(N))
orders_at(order.group).clear
end
def execute_orders! : Array(Order(N))
executed = Array(Order(N)).new
@orders.each do |group, orders|
executed_one = execute_orders_group!(orders)
executed << executed_one if !executed_one.nil?
end
executed
end
# Execute only the most prioritized order and remove it from the order list.
# This order may trigger a clear operation if it cancels non executed orders.
private def execute_orders_group!(orders : Array(Order(N))) : Order(N)?
return nil if orders.empty?
orders.sort!
order = orders.pop # remove last order and execute it
order.execute
# orders.unshift order if order.keep
order
end
end
# And Order is supposed to write something on the vessel state.
# It may be accelerate the vector, use communications antennas, sensors, etc.
# And order is executed when executed is called, which allows for the DLS code:
#
# value = 123.45
# vessel.pilot << Order(2).new(priority: 2, group: :acceleration) { |order|
# order.vessel.acceleration[0] = value
# order.station.clear_next!(order)
# }
#
class Order(N)
setter :station
getter :priority, :group
# property :keep
def initialize(@priority : Int32 = 0, @group : Symbol = :default, @station : Station(N)? = nil, @keep : Bool = false, &@block : Order(N) -> Nil)
end
def to_s(io : IO) : Nil
io << "Order[#{station.class}:#{group}:#{priority}]"
end
def pretty_print(pp)
pp.text(to_s)
end
# No order should have a nil station, but there is a very small window in the code where
# the station is nil during initialization before being attributed to the station via `Station#<<`.
def station : Station(N)
station = @station
raise "No station set, should not happen" if station.nil?
station
end
# shortcut
def vessel
station.vessel
end
def execute
@block.call(self)
end
# Compare priorities of 2 `Order`
def <=>(right)
return 0 if @group != right.group # should not happen... raise an exception ?
@priority <=> right.priority
end
end
class Pilot(N) < Station(N)
# these properties handle manual override of trust (either analogic or exact computed)
property low_thrust_save, low_thrust_pressed, computer_input_thrust
# on the interface, the pilot can select a body to create relative vectors
property current_selected_body : Gravity::Body(N)?
# this is a hard limit for thrust so the pilot can avoid killing all the people aboard
property max_thrust
# permanent mode is the default modes to apply at all time
property permanent_modes : Array(Mode)
# temporary mode must be set at each tick and will be reset each time it is applied
property temporary_modes : Array(Mode)
enum Mode
Constant
Pulse
AntiGrav
AntiSpeed
end
def initialize(*p, **o)
super
@permanent_modes = Array(Mode).new
@temporary_modes = Array(Mode).new
@low_thrust_save = 0.0
@low_thrust_pressed = false
@computer_input_thrust = Vector(Float64, N).zero
@max_thrust = 20.0
@current_selected_body = nil
end
# Modify immediatly one elemnt of the acceleration vector
def set_exact_acceleration_axis(index : Int32, value : Float64)
vessel.gravity_body.acceleration[index] = value
end
# Modify immediatly the acceleration vector
def set_exact_acceleration_axis(axis : Vector(Float64, N))
axis.each_with_index do |value, index|
set_exact_acceleration_axis(index, value)
end
end
# set_exact_acceleration_axis({0, 23.0}, {1, 11.2}) # to set x;y at once
def set_exact_acceleration_axis(*tuples : Array(Tuple(Int32, Float64)))
tuples.each do |tuple|
set_exact_acceleration_axis(*tuple)
end
end
def immediate_modes : Array(Mode)
if !@temporary_modes.empty?
@temporary_modes
else
@permanent_modes
end
end
def apply_current_mode
new_acceleration_axis = Vector(Float64, N).zero
must_update_acceleration_axis = false
if immediate_modes.includes?(Mode::AntiGrav)
must_update_acceleration_axis = true
new_acceleration_axis.add!(-vessel.gravity)
end
if immediate_modes.includes?(Mode::AntiSpeed)
must_update_acceleration_axis = true
reset_acceleration = true
if (current_selected_body = @current_selected_body)
relative_speed = vessel.gravity_body.speed - current_selected_body.speed
new_acceleration_axis.add!(-relative_speed)
end
end
if new_acceleration_axis.magnitude > @max_thrust
new_acceleration_axis.normalize.mult!(@max_thrust)
end
set_exact_acceleration_axis(new_acceleration_axis) if must_update_acceleration_axis
@temporary_modes.clear # each time we apply the temporary_mode, reset it
end
def accelerate_current_vector!(coef : Float64)
set_exact_acceleration_axis(vessel.gravity_body.speed * coef)
end
end
class Navigation(N) < Station(N)
class Cadran
property radius : Array(Float64)
def initialize(default_radius : Float64 = 1000.0)
@radius = [] of Float64
@radius << default_radius if !default_radius.nil?
end
end
property zoom_select : Int32
property zoom_position : Vector(Float64, N)
property cadran : Cadran
property zoom_ratio : Float64
def initialize(*p, **o)
super
@zoom_ratio = 1.0
@zoom_select = 0
@zoom_position = Vector(Float64, N).zero
@cadran = Cadran.new
end
end
end