Implement production vertical slice foundation
This commit is contained in:
1 parent
9d4a89f355
commit
a18e82eb44
104 files changed
+10595
-268
No files matched your search
@@ -0,0 +1,370 @@
|
||||
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
|
||||
Reference in new issue
Block a user