371 lines
13 KiB
GDScript
371 lines
13 KiB
GDScript
class_name DiveSession
|
|
extends RefCounted
|
|
|
|
const ResolvedDiveRules = preload("res://game/domain/rules/resolved_dive_rules.gd")
|
|
const DiveStep = preload("res://game/domain/dive/dive_step.gd")
|
|
const CollisionObservation = preload("res://game/domain/dive/collision_observation.gd")
|
|
const BatterySnapshot = preload("res://game/domain/dive/battery_snapshot.gd")
|
|
const OxygenSnapshot = preload("res://game/domain/dive/oxygen_snapshot.gd")
|
|
const HullSnapshot = preload("res://game/domain/dive/hull_snapshot.gd")
|
|
const DepthSnapshot = preload("res://game/domain/dive/depth_snapshot.gd")
|
|
const ReturnEstimate = preload("res://game/domain/dive/return_estimate.gd")
|
|
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
|
|
const DiveAlertState = preload("res://game/domain/dive/dive_alert_state.gd")
|
|
const EjectionSnapshot = preload("res://game/domain/dive/ejection_snapshot.gd")
|
|
const DiveTerminalReason = preload("res://game/domain/dive/dive_terminal_reason.gd")
|
|
const DiveSnapshot = preload("res://game/domain/dive/dive_snapshot.gd")
|
|
const DiveEvent = preload("res://game/domain/dive/dive_event.gd")
|
|
const DiveUpdate = preload("res://game/domain/dive/dive_update.gd")
|
|
|
|
var session_id: StringName
|
|
var rules: ResolvedDiveRules
|
|
var initialization_error: String = ""
|
|
|
|
var _battery_kwh: float
|
|
var _oxygen_l: float
|
|
var _hull_integrity: float
|
|
var _depth_m := 0.0
|
|
var _current_power_kw := 0.0
|
|
var _ejection_held_seconds := 0.0
|
|
var _crush_exposure_seconds := 0.0
|
|
var _collision_cooldown_seconds := 0.0
|
|
var _last_tick_sequence := -1
|
|
var _terminal_reason := DiveTerminalReason.Value.NONE
|
|
var _return_estimate: ReturnEstimate = ReturnEstimate.unknown()
|
|
var _processed_collision_ids: Dictionary[StringName, bool] = {}
|
|
var _alerts: DiveAlertState
|
|
var _snapshot: DiveSnapshot
|
|
|
|
|
|
func _init(
|
|
p_session_id: StringName,
|
|
p_rules: ResolvedDiveRules,
|
|
initial_battery_kwh: float = -1.0,
|
|
initial_oxygen_l: float = -1.0,
|
|
initial_hull_integrity: float = -1.0
|
|
) -> void:
|
|
session_id = p_session_id
|
|
rules = p_rules
|
|
initialization_error = _validate_initialization(
|
|
initial_battery_kwh,
|
|
initial_oxygen_l,
|
|
initial_hull_integrity
|
|
)
|
|
if not initialization_error.is_empty():
|
|
return
|
|
_battery_kwh = (
|
|
rules.battery_capacity_kwh if initial_battery_kwh < 0.0 else initial_battery_kwh
|
|
)
|
|
_oxygen_l = rules.oxygen_capacity_l if initial_oxygen_l < 0.0 else initial_oxygen_l
|
|
_hull_integrity = (
|
|
rules.hull_max_integrity if initial_hull_integrity < 0.0 else initial_hull_integrity
|
|
)
|
|
_alerts = _build_alert_state()
|
|
_snapshot = _build_snapshot()
|
|
|
|
|
|
func is_ready() -> bool:
|
|
return initialization_error.is_empty()
|
|
|
|
|
|
func current_snapshot() -> DiveSnapshot:
|
|
return _snapshot
|
|
|
|
|
|
func advance(step: DiveStep) -> DiveUpdate:
|
|
var requested_sequence := step.tick.sequence if step != null and step.tick != null else -1
|
|
if not is_ready():
|
|
return DiveUpdate.rejected(requested_sequence, initialization_error, _snapshot)
|
|
if _terminal_reason != DiveTerminalReason.Value.NONE:
|
|
return DiveUpdate.rejected(requested_sequence, "dive session is terminal", _snapshot)
|
|
if step == null:
|
|
return DiveUpdate.rejected(requested_sequence, "DiveStep is null", _snapshot)
|
|
var validation_error: String = step.validation_error()
|
|
if not validation_error.is_empty():
|
|
return DiveUpdate.rejected(requested_sequence, validation_error, _snapshot)
|
|
if step.tick.session_id != session_id:
|
|
return DiveUpdate.rejected(requested_sequence, "tick session_id does not match", _snapshot)
|
|
if step.tick.sequence != _last_tick_sequence + 1:
|
|
return DiveUpdate.rejected(requested_sequence, "tick sequence is not consecutive", _snapshot)
|
|
|
|
var previous_battery := _battery_kwh
|
|
var previous_oxygen := _oxygen_l
|
|
var previous_hull := _hull_integrity
|
|
var previous_alerts := _alerts
|
|
var delta_seconds := step.tick.elapsed_seconds
|
|
var events: Array[DiveEvent] = []
|
|
|
|
_last_tick_sequence = step.tick.sequence
|
|
_depth_m = step.telemetry.depth_m
|
|
_current_power_kw = _calculate_power_kw(step)
|
|
_battery_kwh = maxf(
|
|
0.0,
|
|
_battery_kwh - _current_power_kw * delta_seconds / 3600.0
|
|
)
|
|
_oxygen_l = maxf(
|
|
0.0,
|
|
_oxygen_l - rules.oxygen_consumption_l_per_min * delta_seconds / 60.0
|
|
)
|
|
|
|
var hull_damage := _apply_pressure_damage(delta_seconds)
|
|
hull_damage += _apply_collision_damage(step, delta_seconds)
|
|
if hull_damage > 0.0:
|
|
events.append(DiveEvent.hull_damaged(_last_tick_sequence, hull_damage))
|
|
|
|
if step.ejection_intent.confirmed:
|
|
_ejection_held_seconds = rules.ejection_hold_seconds
|
|
elif step.ejection_intent.active:
|
|
_ejection_held_seconds = minf(
|
|
rules.ejection_hold_seconds,
|
|
_ejection_held_seconds + delta_seconds
|
|
)
|
|
else:
|
|
_ejection_held_seconds = 0.0
|
|
|
|
_return_estimate = _calculate_return_estimate(step)
|
|
_alerts = _build_alert_state()
|
|
_append_depletion_events(events, previous_battery, previous_oxygen, previous_hull)
|
|
_append_alert_events(events, previous_alerts)
|
|
_update_terminal_reason()
|
|
if _terminal_reason != DiveTerminalReason.Value.NONE:
|
|
events.append(DiveEvent.dive_terminated(_last_tick_sequence, _terminal_reason))
|
|
|
|
_snapshot = _build_snapshot()
|
|
return DiveUpdate.new(true, _last_tick_sequence, "", _snapshot, events)
|
|
|
|
|
|
func _validate_initialization(
|
|
initial_battery_kwh: float,
|
|
initial_oxygen_l: float,
|
|
initial_hull_integrity: float
|
|
) -> String:
|
|
if session_id.is_empty():
|
|
return "session_id is empty"
|
|
if rules == null:
|
|
return "resolved dive rules are missing"
|
|
var rules_error: String = rules.validation_error()
|
|
if not rules_error.is_empty():
|
|
return rules_error
|
|
if not _valid_initial_value(initial_battery_kwh, rules.battery_capacity_kwh):
|
|
return "initial battery is outside resolved capacity"
|
|
if not _valid_initial_value(initial_oxygen_l, rules.oxygen_capacity_l):
|
|
return "initial oxygen is outside resolved capacity"
|
|
if not _valid_initial_value(initial_hull_integrity, rules.hull_max_integrity):
|
|
return "initial hull is outside resolved capacity"
|
|
return ""
|
|
|
|
|
|
func _valid_initial_value(value: float, maximum: float) -> bool:
|
|
return is_finite(value) and (is_equal_approx(value, -1.0) or (value >= 0.0 and value <= maximum))
|
|
|
|
|
|
func _calculate_power_kw(step: DiveStep) -> float:
|
|
var power := (
|
|
absf(step.telemetry.longitudinal_effort) * rules.longitudinal_power_kw
|
|
+ absf(step.telemetry.steering_effort) * rules.steering_power_kw
|
|
+ absf(step.telemetry.vertical_effort) * rules.vertical_power_kw
|
|
)
|
|
if step.telemetry.high_thrust_active:
|
|
power += rules.high_thrust_power_kw
|
|
for demand in step.tool_power_demands:
|
|
power += demand.effective_power_kw
|
|
return power
|
|
|
|
|
|
func _apply_pressure_damage(delta_seconds: float) -> float:
|
|
var damage_rate := 0.0
|
|
if _depth_m <= rules.certified_depth_m:
|
|
_crush_exposure_seconds = 0.0
|
|
elif _depth_m <= rules.critical_depth_m:
|
|
_crush_exposure_seconds = 0.0
|
|
var range_size := rules.critical_depth_m - rules.certified_depth_m
|
|
var ratio := (_depth_m - rules.certified_depth_m) / range_size
|
|
damage_rate = lerpf(0.15, 5.0, ratio * ratio)
|
|
elif _depth_m <= rules.crush_depth_m:
|
|
_crush_exposure_seconds = 0.0
|
|
var range_size := rules.crush_depth_m - rules.critical_depth_m
|
|
var ratio := (_depth_m - rules.critical_depth_m) / range_size
|
|
damage_rate = lerpf(5.0, 18.0, ratio)
|
|
else:
|
|
_crush_exposure_seconds += delta_seconds
|
|
if _crush_exposure_seconds >= rules.crush_grace_seconds:
|
|
damage_rate = 100.0
|
|
|
|
var damage := damage_rate * rules.pressure_damage_multiplier * delta_seconds
|
|
return _damage_hull(damage)
|
|
|
|
|
|
func _apply_collision_damage(step: DiveStep, delta_seconds: float) -> float:
|
|
_collision_cooldown_seconds = maxf(0.0, _collision_cooldown_seconds - delta_seconds)
|
|
var ordered_collisions: Array[CollisionObservation] = step.collisions.duplicate()
|
|
ordered_collisions.sort_custom(_collision_comes_first)
|
|
var total_damage := 0.0
|
|
for collision: CollisionObservation in ordered_collisions:
|
|
if _processed_collision_ids.has(collision.observation_id):
|
|
continue
|
|
_processed_collision_ids[collision.observation_id] = true
|
|
if collision.normal_impact_speed_mps <= rules.collision_speed_threshold_mps:
|
|
continue
|
|
if _collision_cooldown_seconds > 0.0:
|
|
continue
|
|
var excess_speed := (
|
|
collision.normal_impact_speed_mps - rules.collision_speed_threshold_mps
|
|
)
|
|
var requested_damage := (
|
|
excess_speed
|
|
* rules.collision_damage_per_excess_mps
|
|
* rules.collision_damage_multiplier
|
|
)
|
|
total_damage += _damage_hull(requested_damage)
|
|
_collision_cooldown_seconds = rules.collision_repeat_delay_seconds
|
|
return total_damage
|
|
|
|
|
|
func _collision_comes_first(left: CollisionObservation, right: CollisionObservation) -> bool:
|
|
return left.sequence < right.sequence
|
|
|
|
|
|
func _damage_hull(requested_damage: float) -> float:
|
|
if requested_damage <= 0.0:
|
|
return 0.0
|
|
var applied_damage := minf(requested_damage, _hull_integrity)
|
|
_hull_integrity -= applied_damage
|
|
return applied_damage
|
|
|
|
|
|
func _calculate_return_estimate(step: DiveStep) -> ReturnEstimate:
|
|
if not step.return_route.is_known:
|
|
return ReturnEstimate.unknown()
|
|
var required := (
|
|
step.return_route.distance_m * rules.return_energy_kwh_per_m
|
|
+ step.return_route.ascent_m * rules.ascent_energy_kwh_per_m
|
|
)
|
|
var margin := _battery_kwh - required
|
|
var margin_ratio := margin / rules.battery_capacity_kwh
|
|
var status := ReturnEstimate.Status.SAFE
|
|
if margin < 0.0:
|
|
status = ReturnEstimate.Status.INSUFFICIENT
|
|
elif margin_ratio <= rules.return_ratio:
|
|
status = ReturnEstimate.Status.TIGHT
|
|
return ReturnEstimate.new(required, margin, margin_ratio, status)
|
|
|
|
|
|
func _append_depletion_events(
|
|
events: Array[DiveEvent],
|
|
previous_battery: float,
|
|
previous_oxygen: float,
|
|
previous_hull: float
|
|
) -> void:
|
|
if previous_battery > 0.0 and _battery_kwh <= 0.0:
|
|
events.append(DiveEvent.resource_depleted(_last_tick_sequence, &"battery"))
|
|
if previous_oxygen > 0.0 and _oxygen_l <= 0.0:
|
|
events.append(DiveEvent.resource_depleted(_last_tick_sequence, &"oxygen"))
|
|
if previous_hull > 0.0 and _hull_integrity <= 0.0:
|
|
events.append(DiveEvent.resource_depleted(_last_tick_sequence, &"hull"))
|
|
|
|
|
|
func _append_alert_events(
|
|
events: Array[DiveEvent],
|
|
previous_alerts: DiveAlertState
|
|
) -> void:
|
|
_append_alert_event(events, &"oxygen", previous_alerts.oxygen, _alerts.oxygen)
|
|
_append_alert_event(events, &"battery", previous_alerts.battery, _alerts.battery)
|
|
_append_alert_event(events, &"hull", previous_alerts.hull, _alerts.hull)
|
|
_append_alert_event(events, &"depth", previous_alerts.depth, _alerts.depth)
|
|
|
|
|
|
func _append_alert_event(
|
|
events: Array[DiveEvent],
|
|
subject: StringName,
|
|
previous_level: DiveAlertLevel.Value,
|
|
current_level: DiveAlertLevel.Value
|
|
) -> void:
|
|
if previous_level == current_level:
|
|
return
|
|
events.append(
|
|
DiveEvent.alert_changed(
|
|
_last_tick_sequence,
|
|
subject,
|
|
previous_level,
|
|
current_level
|
|
)
|
|
)
|
|
|
|
|
|
func _update_terminal_reason() -> void:
|
|
if _hull_integrity <= 0.0:
|
|
_terminal_reason = DiveTerminalReason.Value.HULL_DESTROYED
|
|
elif _oxygen_l <= 0.0:
|
|
_terminal_reason = DiveTerminalReason.Value.OXYGEN_DEPLETED
|
|
elif _ejection_held_seconds >= rules.ejection_hold_seconds:
|
|
_terminal_reason = DiveTerminalReason.Value.VOLUNTARY_EJECTION
|
|
|
|
|
|
func _build_alert_state() -> DiveAlertState:
|
|
return DiveAlertState.new(
|
|
_resource_alert(_ratio(_oxygen_l, rules.oxygen_capacity_l)),
|
|
_resource_alert(_ratio(_battery_kwh, rules.battery_capacity_kwh)),
|
|
_resource_alert(_ratio(_hull_integrity, rules.hull_max_integrity)),
|
|
_depth_alert(_depth_m)
|
|
)
|
|
|
|
|
|
func _resource_alert(ratio: float) -> DiveAlertLevel.Value:
|
|
if ratio <= 0.0:
|
|
return DiveAlertLevel.Value.FAILURE
|
|
if ratio <= rules.critical_ratio:
|
|
return DiveAlertLevel.Value.CRITICAL
|
|
if ratio <= rules.return_ratio:
|
|
return DiveAlertLevel.Value.RETURN
|
|
if ratio <= rules.attention_ratio:
|
|
return DiveAlertLevel.Value.ATTENTION
|
|
return DiveAlertLevel.Value.NORMAL
|
|
|
|
|
|
func _depth_alert(depth_m: float) -> DiveAlertLevel.Value:
|
|
if depth_m >= rules.crush_depth_m:
|
|
return DiveAlertLevel.Value.FAILURE
|
|
if depth_m >= rules.critical_depth_m:
|
|
return DiveAlertLevel.Value.CRITICAL
|
|
if depth_m > rules.certified_depth_m:
|
|
return DiveAlertLevel.Value.RETURN
|
|
if depth_m >= rules.certified_depth_m * 0.85:
|
|
return DiveAlertLevel.Value.ATTENTION
|
|
return DiveAlertLevel.Value.NORMAL
|
|
|
|
|
|
func _build_snapshot() -> DiveSnapshot:
|
|
var exterior_pressure := 1.0 + _depth_m / 10.0
|
|
return DiveSnapshot.new(
|
|
session_id,
|
|
_last_tick_sequence,
|
|
BatterySnapshot.new(_battery_kwh, rules.battery_capacity_kwh, _current_power_kw),
|
|
OxygenSnapshot.new(
|
|
_oxygen_l,
|
|
rules.oxygen_capacity_l,
|
|
rules.oxygen_consumption_l_per_min
|
|
),
|
|
HullSnapshot.new(
|
|
_hull_integrity,
|
|
rules.hull_max_integrity,
|
|
exterior_pressure,
|
|
rules.certified_pressure_bar,
|
|
rules.critical_pressure_bar,
|
|
rules.crush_pressure_bar
|
|
),
|
|
DepthSnapshot.new(
|
|
_depth_m,
|
|
rules.certified_depth_m,
|
|
rules.critical_depth_m,
|
|
rules.crush_depth_m
|
|
),
|
|
_return_estimate,
|
|
_alerts,
|
|
EjectionSnapshot.new(_ejection_held_seconds, rules.ejection_hold_seconds),
|
|
_terminal_reason
|
|
)
|
|
|
|
|
|
func _ratio(current: float, maximum: float) -> float:
|
|
return current / maximum if maximum > 0.0 else 0.0
|