Implement production vertical slice foundation

This commit is contained in:
chindit committed 2026-10-02 15:27:48 +02:00
1 parent 9d4a89f355
commit a18e82eb44
104 files changed
+10595 -268

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
class_name BatterySnapshot
extends RefCounted
var remaining_kwh: float
var maximum_kwh: float
var ratio: float
var current_power_kw: float
var endurance_minutes: float
func _init(
p_remaining_kwh: float,
p_maximum_kwh: float,
p_current_power_kw: float
) -> void:
remaining_kwh = p_remaining_kwh
maximum_kwh = p_maximum_kwh
ratio = remaining_kwh / maximum_kwh if maximum_kwh > 0.0 else 0.0
current_power_kw = p_current_power_kw
endurance_minutes = (
remaining_kwh / current_power_kw * 60.0 if current_power_kw > 0.0 else INF
)
+31
View File
@@ -0,0 +1,31 @@
class_name CollisionObservation
extends RefCounted
var observation_id: StringName
var sequence: int
var tick_sequence: int
var normal_impact_speed_mps: float
func _init(
p_observation_id: StringName,
p_sequence: int,
p_tick_sequence: int,
p_normal_impact_speed_mps: float
) -> void:
observation_id = p_observation_id
sequence = p_sequence
tick_sequence = p_tick_sequence
normal_impact_speed_mps = p_normal_impact_speed_mps
func validation_error(expected_tick_sequence: int) -> String:
if observation_id.is_empty():
return "collision observation_id is empty"
if sequence < 0:
return "collision sequence is negative"
if tick_sequence != expected_tick_sequence:
return "collision tick sequence does not match DiveTick"
if not is_finite(normal_impact_speed_mps) or normal_impact_speed_mps < 0.0:
return "collision speed must be finite and non-negative"
return ""
+19
View File
@@ -0,0 +1,19 @@
class_name DepthSnapshot
extends RefCounted
var current_m: float
var certified_m: float
var critical_m: float
var crush_m: float
func _init(
p_current_m: float,
p_certified_m: float,
p_critical_m: float,
p_crush_m: float
) -> void:
current_m = p_current_m
certified_m = p_certified_m
critical_m = p_critical_m
crush_m = p_crush_m
+18
View File
@@ -0,0 +1,18 @@
class_name DiveAlertLevel
extends RefCounted
enum Value {
NORMAL,
ATTENTION,
RETURN,
CRITICAL,
FAILURE,
}
static func highest(levels: Array[Value]) -> Value:
var result := Value.NORMAL
for level: Value in levels:
if level > result:
result = level
return result
+23
View File
@@ -0,0 +1,23 @@
class_name DiveAlertState
extends RefCounted
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
var oxygen: DiveAlertLevel.Value
var battery: DiveAlertLevel.Value
var hull: DiveAlertLevel.Value
var depth: DiveAlertLevel.Value
var maximum: DiveAlertLevel.Value
func _init(
p_oxygen: DiveAlertLevel.Value,
p_battery: DiveAlertLevel.Value,
p_hull: DiveAlertLevel.Value,
p_depth: DiveAlertLevel.Value
) -> void:
oxygen = p_oxygen
battery = p_battery
hull = p_hull
depth = p_depth
maximum = DiveAlertLevel.highest([oxygen, battery, hull, depth])
+101
View File
@@ -0,0 +1,101 @@
class_name DiveEvent
extends RefCounted
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
const DiveTerminalReason = preload("res://game/domain/dive/dive_terminal_reason.gd")
enum Kind {
ALERT_LEVEL_CHANGED,
RESOURCE_DEPLETED,
HULL_DAMAGED,
DIVE_TERMINATED,
}
var event_id: StringName
var kind: Kind
var tick_sequence: int
var subject: StringName
var amount: float
var previous_level: DiveAlertLevel.Value
var current_level: DiveAlertLevel.Value
var terminal_reason: DiveTerminalReason.Value
func _init(
p_event_id: StringName,
p_kind: Kind,
p_tick_sequence: int,
p_subject: StringName = &"",
p_amount: float = 0.0,
p_previous_level: DiveAlertLevel.Value = DiveAlertLevel.Value.NORMAL,
p_current_level: DiveAlertLevel.Value = DiveAlertLevel.Value.NORMAL,
p_terminal_reason: DiveTerminalReason.Value = DiveTerminalReason.Value.NONE
) -> void:
event_id = p_event_id
kind = p_kind
tick_sequence = p_tick_sequence
subject = p_subject
amount = p_amount
previous_level = p_previous_level
current_level = p_current_level
terminal_reason = p_terminal_reason
static func alert_changed(
tick_sequence: int,
subject: StringName,
previous_level: DiveAlertLevel.Value,
current_level: DiveAlertLevel.Value
):
return new(
_event_id(tick_sequence, &"alert", subject),
Kind.ALERT_LEVEL_CHANGED,
tick_sequence,
subject,
0.0,
previous_level,
current_level
)
static func resource_depleted(tick_sequence: int, subject: StringName):
return new(
_event_id(tick_sequence, &"depleted", subject),
Kind.RESOURCE_DEPLETED,
tick_sequence,
subject
)
static func hull_damaged(tick_sequence: int, amount: float):
return new(
_event_id(tick_sequence, &"damage", &"hull"),
Kind.HULL_DAMAGED,
tick_sequence,
&"hull",
amount
)
static func dive_terminated(
tick_sequence: int,
reason: DiveTerminalReason.Value
):
return new(
_event_id(tick_sequence, &"terminal", &"dive"),
Kind.DIVE_TERMINATED,
tick_sequence,
&"dive",
0.0,
DiveAlertLevel.Value.NORMAL,
DiveAlertLevel.Value.NORMAL,
reason
)
static func _event_id(
tick_sequence: int,
event_name: StringName,
subject_name: StringName
) -> StringName:
return StringName("%d:%s:%s" % [tick_sequence, event_name, subject_name])
+370
View File
@@ -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
+50
View File
@@ -0,0 +1,50 @@
class_name DiveSnapshot
extends RefCounted
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 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")
var session_id: StringName
var tick_sequence: int
var battery: BatterySnapshot
var oxygen: OxygenSnapshot
var hull: HullSnapshot
var depth: DepthSnapshot
var return_estimate: ReturnEstimate
var alerts: DiveAlertState
var ejection: EjectionSnapshot
var terminal_reason: DiveTerminalReason.Value
func _init(
p_session_id: StringName,
p_tick_sequence: int,
p_battery: BatterySnapshot,
p_oxygen: OxygenSnapshot,
p_hull: HullSnapshot,
p_depth: DepthSnapshot,
p_return_estimate: ReturnEstimate,
p_alerts: DiveAlertState,
p_ejection: EjectionSnapshot,
p_terminal_reason: DiveTerminalReason.Value
) -> void:
session_id = p_session_id
tick_sequence = p_tick_sequence
battery = p_battery
oxygen = p_oxygen
hull = p_hull
depth = p_depth
return_estimate = p_return_estimate
alerts = p_alerts
ejection = p_ejection
terminal_reason = p_terminal_reason
func is_terminal() -> bool:
return terminal_reason != DiveTerminalReason.Value.NONE
+70
View File
@@ -0,0 +1,70 @@
class_name DiveStep
extends RefCounted
const DiveTick = preload("res://game/domain/dive/dive_tick.gd")
const DiveTelemetrySample = preload("res://game/domain/dive/dive_telemetry_sample.gd")
const CollisionObservation = preload("res://game/domain/dive/collision_observation.gd")
const ToolPowerDemand = preload("res://game/domain/dive/tool_power_demand.gd")
const EjectionIntent = preload("res://game/domain/dive/ejection_intent.gd")
const ReturnRouteSample = preload("res://game/domain/dive/return_route_sample.gd")
var tick: DiveTick
var telemetry: DiveTelemetrySample
var collisions: Array[CollisionObservation]
var tool_power_demands: Array[ToolPowerDemand]
var ejection_intent: EjectionIntent
var return_route: ReturnRouteSample
func _init(
p_tick: DiveTick,
p_telemetry: DiveTelemetrySample,
p_collisions: Array[CollisionObservation] = [],
p_tool_power_demands: Array[ToolPowerDemand] = [],
p_ejection_intent: EjectionIntent = null,
p_return_route: ReturnRouteSample = null
) -> void:
tick = p_tick
telemetry = p_telemetry
collisions = p_collisions.duplicate()
tool_power_demands = p_tool_power_demands.duplicate()
ejection_intent = p_ejection_intent if p_ejection_intent != null else EjectionIntent.new()
return_route = p_return_route if p_return_route != null else ReturnRouteSample.new()
func validation_error() -> String:
if tick == null:
return "DiveStep has no tick"
var error: String = tick.validation_error()
if not error.is_empty():
return error
if telemetry == null:
return "DiveStep has no telemetry"
error = telemetry.validation_error()
if not error.is_empty():
return error
if ejection_intent == null:
return "DiveStep has no ejection intent"
if return_route == null:
return "DiveStep has no return route sample"
error = return_route.validation_error()
if not error.is_empty():
return error
for collision: CollisionObservation in collisions:
if collision == null:
return "DiveStep contains a null collision"
error = collision.validation_error(tick.sequence)
if not error.is_empty():
return error
for demand: ToolPowerDemand in tool_power_demands:
if demand == null:
return "DiveStep contains a null tool power demand"
error = demand.validation_error()
if not error.is_empty():
return error
var tool_ids: Dictionary[StringName, bool] = {}
for demand: ToolPowerDemand in tool_power_demands:
if tool_ids.has(demand.tool_id):
return "DiveStep contains duplicate tool power demands"
tool_ids[demand.tool_id] = true
return ""
+38
View File
@@ -0,0 +1,38 @@
class_name DiveTelemetrySample
extends RefCounted
var depth_m: float
var longitudinal_effort: float
var steering_effort: float
var vertical_effort: float
var high_thrust_active: bool
func _init(
p_depth_m: float = 0.0,
p_longitudinal_effort: float = 0.0,
p_steering_effort: float = 0.0,
p_vertical_effort: float = 0.0,
p_high_thrust_active: bool = false
) -> void:
depth_m = p_depth_m
longitudinal_effort = p_longitudinal_effort
steering_effort = p_steering_effort
vertical_effort = p_vertical_effort
high_thrust_active = p_high_thrust_active
func validation_error() -> String:
if not is_finite(depth_m) or depth_m < 0.0:
return "telemetry depth must be finite and non-negative"
if not _is_normalized(longitudinal_effort):
return "longitudinal effort must be between -1 and 1"
if not _is_normalized(steering_effort):
return "steering effort must be between -1 and 1"
if not _is_normalized(vertical_effort):
return "vertical effort must be between -1 and 1"
return ""
func _is_normalized(value: float) -> bool:
return is_finite(value) and value >= -1.0 and value <= 1.0
+10
View File
@@ -0,0 +1,10 @@
class_name DiveTerminalReason
extends RefCounted
enum Value {
NONE,
RETURNED,
VOLUNTARY_EJECTION,
OXYGEN_DEPLETED,
HULL_DESTROYED,
}
+28
View File
@@ -0,0 +1,28 @@
class_name DiveTick
extends RefCounted
var session_id: StringName
var sequence: int
var elapsed_seconds: float
func _init(
p_session_id: StringName,
p_sequence: int,
p_elapsed_seconds: float
) -> void:
session_id = p_session_id
sequence = p_sequence
elapsed_seconds = p_elapsed_seconds
func validation_error() -> String:
if session_id.is_empty():
return "tick session_id is empty"
if sequence < 0:
return "tick sequence is negative"
if not is_finite(elapsed_seconds) or elapsed_seconds <= 0.0:
return "tick duration must be finite and positive"
if elapsed_seconds > 0.1:
return "tick duration exceeds 0.1 seconds"
return ""
+37
View File
@@ -0,0 +1,37 @@
class_name DiveUpdate
extends RefCounted
const DiveSnapshot = preload("res://game/domain/dive/dive_snapshot.gd")
const DiveEvent = preload("res://game/domain/dive/dive_event.gd")
var accepted: bool
var requested_tick_sequence: int
var rejection_reason: String
var snapshot: DiveSnapshot
var events: Array[DiveEvent]
func _init(
p_accepted: bool,
p_requested_tick_sequence: int,
p_rejection_reason: String,
p_snapshot: DiveSnapshot,
p_events: Array[DiveEvent] = []
) -> void:
accepted = p_accepted
requested_tick_sequence = p_requested_tick_sequence
rejection_reason = p_rejection_reason
snapshot = p_snapshot
events = p_events.duplicate()
static func initial(snapshot: DiveSnapshot):
return new(true, snapshot.tick_sequence, "", snapshot)
static func rejected(
requested_tick_sequence: int,
reason: String,
snapshot: DiveSnapshot
):
return new(false, requested_tick_sequence, reason, snapshot)
+10
View File
@@ -0,0 +1,10 @@
class_name EjectionIntent
extends RefCounted
var active: bool
var confirmed: bool
func _init(p_active: bool = false, p_confirmed: bool = false) -> void:
active = p_active
confirmed = p_confirmed
+12
View File
@@ -0,0 +1,12 @@
class_name EjectionSnapshot
extends RefCounted
var held_seconds: float
var required_seconds: float
var ratio: float
func _init(p_held_seconds: float, p_required_seconds: float) -> void:
held_seconds = p_held_seconds
required_seconds = p_required_seconds
ratio = clampf(held_seconds / required_seconds, 0.0, 1.0) if required_seconds > 0.0 else 0.0
+27
View File
@@ -0,0 +1,27 @@
class_name HullSnapshot
extends RefCounted
var current_integrity: float
var maximum_integrity: float
var ratio: float
var exterior_pressure_bar: float
var certified_pressure_bar: float
var critical_pressure_bar: float
var crush_pressure_bar: float
func _init(
p_current_integrity: float,
p_maximum_integrity: float,
p_exterior_pressure_bar: float,
p_certified_pressure_bar: float,
p_critical_pressure_bar: float,
p_crush_pressure_bar: float
) -> void:
current_integrity = p_current_integrity
maximum_integrity = p_maximum_integrity
ratio = current_integrity / maximum_integrity if maximum_integrity > 0.0 else 0.0
exterior_pressure_bar = p_exterior_pressure_bar
certified_pressure_bar = p_certified_pressure_bar
critical_pressure_bar = p_critical_pressure_bar
crush_pressure_bar = p_crush_pressure_bar
+22
View File
@@ -0,0 +1,22 @@
class_name OxygenSnapshot
extends RefCounted
var remaining_l: float
var maximum_l: float
var ratio: float
var consumption_l_per_min: float
var endurance_minutes: float
func _init(
p_remaining_l: float,
p_maximum_l: float,
p_consumption_l_per_min: float
) -> void:
remaining_l = p_remaining_l
maximum_l = p_maximum_l
ratio = remaining_l / maximum_l if maximum_l > 0.0 else 0.0
consumption_l_per_min = p_consumption_l_per_min
endurance_minutes = (
remaining_l / consumption_l_per_min if consumption_l_per_min > 0.0 else INF
)
+30
View File
@@ -0,0 +1,30 @@
class_name ReturnEstimate
extends RefCounted
enum Status {
UNKNOWN,
SAFE,
TIGHT,
INSUFFICIENT,
}
var required_kwh: float
var margin_kwh: float
var margin_ratio: float
var status: Status
func _init(
p_required_kwh: float,
p_margin_kwh: float,
p_margin_ratio: float,
p_status: Status
) -> void:
required_kwh = p_required_kwh
margin_kwh = p_margin_kwh
margin_ratio = p_margin_ratio
status = p_status
static func unknown():
return new(0.0, 0.0, 0.0, Status.UNKNOWN)
+24
View File
@@ -0,0 +1,24 @@
class_name ReturnRouteSample
extends RefCounted
var is_known: bool
var distance_m: float
var ascent_m: float
func _init(
p_is_known: bool = false,
p_distance_m: float = 0.0,
p_ascent_m: float = 0.0
) -> void:
is_known = p_is_known
distance_m = p_distance_m
ascent_m = p_ascent_m
func validation_error() -> String:
if not is_finite(distance_m) or distance_m < 0.0:
return "return distance must be finite and non-negative"
if not is_finite(ascent_m) or ascent_m < 0.0:
return "return ascent must be finite and non-negative"
return ""
+18
View File
@@ -0,0 +1,18 @@
class_name ToolPowerDemand
extends RefCounted
var tool_id: StringName
var effective_power_kw: float
func _init(p_tool_id: StringName, p_effective_power_kw: float) -> void:
tool_id = p_tool_id
effective_power_kw = p_effective_power_kw
func validation_error() -> String:
if tool_id.is_empty():
return "tool_id is empty"
if not is_finite(effective_power_kw) or effective_power_kw < 0.0:
return "tool power must be finite and non-negative"
return ""