Files

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