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,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
|
||||
)
|
||||
@@ -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 ""
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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])
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ""
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
class_name DiveTerminalReason
|
||||
extends RefCounted
|
||||
|
||||
enum Value {
|
||||
NONE,
|
||||
RETURNED,
|
||||
VOLUNTARY_EJECTION,
|
||||
OXYGEN_DEPLETED,
|
||||
HULL_DESTROYED,
|
||||
}
|
||||
@@ -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 ""
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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 ""
|
||||
@@ -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 ""
|
||||
Reference in new issue
Block a user