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