797 lines
28 KiB
GDScript
797 lines
28 KiB
GDScript
extends Path3D
|
|
|
|
const DISTANCE_KM_PER_UNIT: float = 0.01
|
|
const STATION_STOP_GROUP: StringName = &"railway_station"
|
|
const TRACK_ZERO_SPACING_FIX_DISTANCE: float = 45.0
|
|
const TRACK_ZERO_SPACING_BLEND_DISTANCE: float = 18.0
|
|
const TRACK_ZERO_SPACING_SEARCH_EXTRA: float = 35.0
|
|
const TRACK_ZERO_SPACING_SEARCH_STEP: float = 0.5
|
|
const TRACK_ZERO_SPACING_TARGET_RATIO: float = 0.97
|
|
const TRACK_ZERO_SPACING_MIN_DISTANCE: float = 1.0
|
|
|
|
enum TrainStartMode { RANDOM_POSITION, SPECIFIED_STATION, SPECIFIED_POSITION }
|
|
|
|
@export_group("Train")
|
|
##train speed
|
|
@export var train_speed: float = 6.0
|
|
##if true the train is in motion
|
|
@export var is_inmotion: bool = true
|
|
##the model for the locomotive
|
|
@export var train_model: PackedScene
|
|
##array of wagon scenes
|
|
@export var wagon_pool: Resource
|
|
##if true the number of wagons is random from 1 to wagon_count
|
|
@export var wagon_random_number: bool = true
|
|
##if wagon_random_number = true is used as max wagons
|
|
@export_range(0, 32, 1, "or_greater") var wagon_count: int = 3
|
|
##the distance between two wagons
|
|
@export var wagon_gap: float = 0.4
|
|
##the scale to calculate the distance of the bounds
|
|
@export_range(0.1, 2.0, 0.05, "or_greater") var wagon_spacing_scale: float = 0.5
|
|
##override spacing using this value
|
|
@export var wagon_spacing_override: float = 0.0
|
|
##distance of the rail axes
|
|
@export var axes_distance: float = 3.0
|
|
##discance between rails
|
|
@export var rail_distance: float = 1.2
|
|
##current camera
|
|
@export var cameras: Node3D
|
|
##swing of the train during the ran
|
|
@export_range(0.0, 1.0) var basic_swing: float = 0.1
|
|
|
|
@export_group("Train Start")
|
|
@export_enum("random_position", "specified_station", "specified_position") var train_start_mode: int = TrainStartMode.RANDOM_POSITION
|
|
@export_range(0, 64, 1, "or_greater") var train_start_stop_index: int = 1
|
|
@export var train_start_position: float = 0.0
|
|
@export var train_start_delay_seconds: float = 0.0
|
|
@export var train_start_after_delay: bool = true
|
|
@export var train_start_acceleration_seconds: float = 4.0
|
|
|
|
@export_group("Rails Bulding")
|
|
##distance between sleepers of the rails
|
|
@export var sleepers_distance: float = 1.0
|
|
##nodel for the sleeper of the rail
|
|
@export var sleepers_model: PackedScene
|
|
|
|
@export_group("Manual Controls")
|
|
##max speed
|
|
@export var speed_max: float = 15.0
|
|
##min speed
|
|
@export var speed_min: float = -5.0
|
|
##acceleration
|
|
@export var manual_acceleration: float = 5.0
|
|
|
|
@export_group("Train Stops")
|
|
##if true the train stop the ran on stops
|
|
@export var enable_stops: bool = true
|
|
##if true the train only stops on markers with a generated station nearby
|
|
@export var require_station_for_stop: bool = true
|
|
##max horizontal distance between a stop marker and a station chunk
|
|
@export var station_stop_detection_radius: float = 35.0
|
|
##time of stop
|
|
@export var stop_time: float = 4.0
|
|
##distance when the train start to brake
|
|
@export var brake_distance: float = 15.0
|
|
##restart time
|
|
@export var restart_time: float = 3.0
|
|
##scene for fireworks
|
|
@export var fireworks_scene: PackedScene
|
|
##array of colors for fireworks
|
|
@export var fireworks_colors: Array[Color] = [
|
|
Color(1.0, 0.2, 0.2), # Rosso vivo
|
|
Color(0.2, 1.0, 0.2), # Verde lime
|
|
Color(0.3, 0.5, 1.0), # Blu cielo
|
|
Color(1.0, 0.8, 0.1), # Oro
|
|
Color(0.8, 0.2, 1.0), # Viola
|
|
Color(0.1, 1.0, 0.9) # Ciano
|
|
]
|
|
|
|
var train_instance: Node3D
|
|
var train_progress: float = 0.0
|
|
var swing_time: float = 0.0
|
|
var curve_roll: float = 0.0
|
|
var curve_pitch: float = 0.0
|
|
|
|
var stop_offset: Array[float] = []
|
|
var stops_position: Array[Vector3] = []
|
|
var stop_ongoing: bool = false
|
|
var next_stop_index: int = 0
|
|
var stop_multiply: float = 1.0
|
|
var is_restarting: bool = false
|
|
var tween_restart: Tween
|
|
var wagon_instances: Array[Node3D] = []
|
|
var wagon_progress_offsets: Array[float] = []
|
|
var _initial_is_inmotion: bool = true
|
|
var _is_photo_mode_active: bool = false
|
|
|
|
func _ready() -> void:
|
|
randomize()
|
|
GameState.on_enable_photo_mode_request.connect(func(): _is_photo_mode_active = true)
|
|
GameState.on_disable_photo_mode_request.connect(func(): _is_photo_mode_active = false)
|
|
|
|
_initial_is_inmotion = is_inmotion
|
|
|
|
if curve != null and curve.get_baked_length() > 0:
|
|
build_rails()
|
|
build_train()
|
|
_plan_stops()
|
|
_apply_initial_train_start()
|
|
_apply_train_start_delay()
|
|
else:
|
|
print("WARNING: Draw Path3D for rails!")
|
|
|
|
func _apply_initial_train_start() -> void:
|
|
if train_instance == null or curve == null:
|
|
return
|
|
|
|
var total_length: float = curve.get_baked_length()
|
|
if total_length <= 0.0:
|
|
return
|
|
|
|
match train_start_mode:
|
|
TrainStartMode.RANDOM_POSITION:
|
|
train_progress = randf() * total_length
|
|
_update_next_stop_index_from_progress()
|
|
TrainStartMode.SPECIFIED_STATION:
|
|
if stop_offset.is_empty():
|
|
train_progress = wrapf(train_progress, 0.0, total_length)
|
|
_update_next_stop_index_from_progress()
|
|
else:
|
|
var stop_index: int = clampi(train_start_stop_index, 0, stop_offset.size() - 1)
|
|
train_progress = stop_offset[stop_index]
|
|
_set_next_stop_index_from_current(stop_index)
|
|
TrainStartMode.SPECIFIED_POSITION:
|
|
train_progress = wrapf(train_start_position, 0.0, total_length)
|
|
_update_next_stop_index_from_progress()
|
|
|
|
_snap_train_to_progress()
|
|
|
|
func _apply_train_start_delay() -> void:
|
|
if not _initial_is_inmotion:
|
|
return
|
|
|
|
var should_start_after_delay: bool = train_start_after_delay
|
|
var start_delay_seconds: float = maxf(train_start_delay_seconds, 0.0)
|
|
if start_delay_seconds <= 0.0:
|
|
if not should_start_after_delay:
|
|
is_inmotion = false
|
|
return
|
|
|
|
is_inmotion = false
|
|
await get_tree().create_timer(start_delay_seconds).timeout
|
|
if not is_inside_tree():
|
|
return
|
|
if not should_start_after_delay:
|
|
return
|
|
|
|
is_inmotion = _initial_is_inmotion
|
|
|
|
func _set_next_stop_index_from_current(current_stop_index: int) -> void:
|
|
if stop_offset.is_empty():
|
|
next_stop_index = 0
|
|
return
|
|
|
|
if train_speed >= 0.0:
|
|
next_stop_index = (current_stop_index + 1) % stop_offset.size()
|
|
else:
|
|
next_stop_index = current_stop_index - 1
|
|
if next_stop_index < 0:
|
|
next_stop_index = stop_offset.size() - 1
|
|
|
|
func _update_next_stop_index_from_progress() -> void:
|
|
if stop_offset.is_empty():
|
|
next_stop_index = 0
|
|
return
|
|
|
|
if train_speed >= 0.0:
|
|
next_stop_index = 0
|
|
for i in range(stop_offset.size()):
|
|
if stop_offset[i] > train_progress + 0.001:
|
|
next_stop_index = i
|
|
return
|
|
return
|
|
|
|
next_stop_index = stop_offset.size() - 1
|
|
for i in range(stop_offset.size() - 1, -1, -1):
|
|
if stop_offset[i] < train_progress - 0.001:
|
|
next_stop_index = i
|
|
return
|
|
|
|
func _snap_train_to_progress() -> void:
|
|
if train_instance == null or curve == null:
|
|
return
|
|
|
|
var total_length: float = curve.get_baked_length()
|
|
if total_length <= 0.0:
|
|
return
|
|
|
|
train_progress = wrapf(train_progress, 0.0, total_length)
|
|
var center_position: Vector3 = to_global(curve.sample_baked(train_progress, true))
|
|
var prog_forward: float = wrapf(train_progress + 2.0, 0.0, total_length)
|
|
var forward_position: Vector3 = to_global(curve.sample_baked(prog_forward, true))
|
|
|
|
train_instance.global_position = center_position
|
|
if center_position.distance_to(forward_position) > 0.01:
|
|
train_instance.look_at(forward_position, Vector3.UP)
|
|
train_instance.rotate_y(PI)
|
|
|
|
if cameras:
|
|
cameras.global_position = center_position
|
|
cameras.global_basis = train_instance.global_basis
|
|
|
|
_snap_wagons_to_progress(total_length)
|
|
|
|
func build_rails() -> void:
|
|
var mat_wood = StandardMaterial3D.new()
|
|
mat_wood.albedo_color = Color(0.35, 0.2, 0.1)
|
|
var mat_iron = StandardMaterial3D.new()
|
|
mat_iron.albedo_color = Color(0.6, 0.6, 0.65)
|
|
mat_iron.metallic = 0.8
|
|
|
|
var mesh_sleeper = BoxMesh.new()
|
|
mesh_sleeper.size = Vector3(rail_distance + 0.6, 0.1, 0.3)
|
|
var mesh_rail = BoxMesh.new()
|
|
mesh_rail.size = Vector3(0.1, 0.15, sleepers_distance + 0.05)
|
|
|
|
var total_length = curve.get_baked_length()
|
|
var piece_number = int(total_length / sleepers_distance)
|
|
|
|
for i in range(piece_number):
|
|
var distance = i * sleepers_distance
|
|
var rail_piece = Node3D.new()
|
|
add_child(rail_piece)
|
|
|
|
var local_pos = curve.sample_baked(distance, true)
|
|
var distance_forward = distance + 0.1
|
|
var local_pos_forward = Vector3.ZERO
|
|
|
|
if distance_forward > total_length:
|
|
var pos_back = curve.sample_baked(distance - 0.1, true)
|
|
local_pos_forward = local_pos + (local_pos - pos_back)
|
|
else:
|
|
local_pos_forward = curve.sample_baked(distance_forward, true)
|
|
|
|
rail_piece.position = local_pos
|
|
var global_pos = to_global(local_pos)
|
|
var global_pos_forward = to_global(local_pos_forward)
|
|
if global_pos.distance_to(global_pos_forward) > 0.001:
|
|
rail_piece.look_at(global_pos_forward, Vector3.UP)
|
|
|
|
if sleepers_model != null:
|
|
var sleeper_custom = sleepers_model.instantiate()
|
|
rail_piece.add_child(sleeper_custom)
|
|
else:
|
|
var sleeper = MeshInstance3D.new()
|
|
sleeper.mesh = mesh_sleeper
|
|
sleeper.material_override = mat_wood
|
|
rail_piece.add_child(sleeper)
|
|
|
|
var rail_sx = MeshInstance3D.new()
|
|
rail_sx.mesh = mesh_rail
|
|
rail_sx.material_override = mat_iron
|
|
rail_sx.position = Vector3(-rail_distance / 2.0, 0.1, 0)
|
|
rail_piece.add_child(rail_sx)
|
|
|
|
var rail_dx = MeshInstance3D.new()
|
|
rail_dx.mesh = mesh_rail
|
|
rail_dx.material_override = mat_iron
|
|
rail_dx.position = Vector3(rail_distance / 2.0, 0.1, 0)
|
|
rail_piece.add_child(rail_dx)
|
|
|
|
func _plan_stops() -> void:
|
|
stop_offset.clear()
|
|
stops_position.clear()
|
|
|
|
var current_stops = []
|
|
for child in get_children():
|
|
if child is Marker3D:
|
|
var offset = curve.get_closest_offset(child.position)
|
|
current_stops.append({
|
|
"offset": offset,
|
|
"position": child.global_position
|
|
})
|
|
current_stops.sort_custom(func(a, b): return a["offset"] < b["offset"])
|
|
for data in current_stops:
|
|
stop_offset.append(data["offset"])
|
|
stops_position.append(data["position"])
|
|
|
|
func _advance_next_stop_index(go_forward: bool) -> void:
|
|
if stop_offset.is_empty():
|
|
next_stop_index = 0
|
|
return
|
|
|
|
if go_forward:
|
|
next_stop_index += 1
|
|
if next_stop_index >= stop_offset.size():
|
|
next_stop_index = 0
|
|
else:
|
|
next_stop_index -= 1
|
|
if next_stop_index < 0:
|
|
next_stop_index = stop_offset.size() - 1
|
|
|
|
func _find_next_valid_stop(go_forward: bool) -> bool:
|
|
if stop_offset.is_empty():
|
|
return false
|
|
|
|
for i in range(stop_offset.size()):
|
|
if _is_stop_valid(next_stop_index):
|
|
return true
|
|
_advance_next_stop_index(go_forward)
|
|
|
|
return false
|
|
|
|
func _is_stop_valid(stop_index: int) -> bool:
|
|
if not require_station_for_stop:
|
|
return true
|
|
if stop_index < 0 or stop_index >= stops_position.size():
|
|
return false
|
|
|
|
return _has_station_near_position(stops_position[stop_index])
|
|
|
|
func _has_station_near_position(stop_position: Vector3) -> bool:
|
|
var radius_sq := station_stop_detection_radius * station_stop_detection_radius
|
|
for station in get_tree().get_nodes_in_group(STATION_STOP_GROUP):
|
|
var station_node := station as Node3D
|
|
if station_node == null or not is_instance_valid(station_node):
|
|
continue
|
|
|
|
var delta := station_node.global_position - stop_position
|
|
var horizontal_dist_sq := delta.x * delta.x + delta.z * delta.z
|
|
if horizontal_dist_sq <= radius_sq:
|
|
return true
|
|
|
|
return false
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if _is_photo_mode_active: return
|
|
if EnvironmentManagerRoot.is_keyboard_input_blocked(get_tree()):
|
|
return
|
|
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
if event.keycode == KEY_T:
|
|
_goto_next_stop()
|
|
if event.is_action_pressed("train_horn"):
|
|
UIEvents.toot_toot.emit(true)
|
|
#elif event.keycode == KEY_F:
|
|
#_test_manual_fireworks()
|
|
|
|
func _test_manual_fireworks() -> void:
|
|
if fireworks_scene == null or train_instance == null: return
|
|
|
|
var firework_number = randi_range(5, 8)
|
|
for i in range(firework_number):
|
|
var fire_root = fireworks_scene.instantiate()
|
|
get_tree().current_scene.add_child(fire_root)
|
|
|
|
var offset_x = randf_range(-6.0, 6.0)
|
|
var offset_z = randf_range(-6.0, 6.0)
|
|
fire_root.global_position = train_instance.global_position + Vector3(offset_x, 0.5, offset_z)
|
|
|
|
if fire_root.has_method("set_color"):
|
|
fire_root.set_color(fireworks_colors.pick_random())
|
|
if fire_root.has_method("turn_on"):
|
|
fire_root.turn_on()
|
|
|
|
await get_tree().create_timer(randf_range(0.2, 0.6)).timeout
|
|
|
|
func _goto_next_stop() -> void:
|
|
if stop_offset.size() == 0 or stop_ongoing or is_restarting:
|
|
return
|
|
if not _find_next_valid_stop(train_speed >= 0.0):
|
|
return
|
|
|
|
var target_offset = stop_offset[next_stop_index]
|
|
train_progress = target_offset
|
|
_execute_stop(train_speed >= 0)
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
input_controls_management(delta)
|
|
train_move(delta)
|
|
|
|
func input_controls_management(delta: float) -> void:
|
|
if EnvironmentManagerRoot.is_keyboard_input_blocked(get_tree()): return
|
|
if not is_inmotion or _is_photo_mode_active: return
|
|
if Input.is_action_pressed("train_speed_up"):
|
|
train_speed += manual_acceleration * delta
|
|
elif Input.is_action_pressed("train_speed_down"):
|
|
train_speed -= manual_acceleration * delta
|
|
elif Input.is_action_pressed("train_stop"):
|
|
train_speed = 0
|
|
train_speed = clamp(train_speed, speed_min, speed_max)
|
|
|
|
func train_move(delta: float) -> void:
|
|
if is_inmotion and train_instance and curve:
|
|
var total_length = curve.get_baked_length()
|
|
if total_length <= 0: return
|
|
|
|
if not stop_ongoing:
|
|
var last_progress = train_progress
|
|
var has_valid_stop := false
|
|
|
|
if enable_stops and stop_offset.size() > 0 and not is_restarting:
|
|
has_valid_stop = _find_next_valid_stop(train_speed >= 0.0)
|
|
if has_valid_stop:
|
|
var target_offset = stop_offset[next_stop_index]
|
|
var dist = target_offset - train_progress
|
|
dist = wrapf(dist + total_length / 2.0, 0.0, total_length) - total_length / 2.0
|
|
|
|
if abs(dist) < brake_distance and sign(dist) == sign(train_speed):
|
|
var t = abs(dist) / brake_distance
|
|
stop_multiply = max(smoothstep(0.0, 1.0, t), 0.05)
|
|
else:
|
|
stop_multiply = 1.0
|
|
else:
|
|
stop_multiply = 1.0
|
|
|
|
var current_speed = train_speed * stop_multiply
|
|
var next_progress: float = train_progress + current_speed * delta
|
|
var wrapped_forward: bool = current_speed > 0.0 and next_progress >= total_length
|
|
var wrapped_back: bool = current_speed < 0.0 and next_progress < 0.0
|
|
train_progress = wrapf(next_progress, 0.0, total_length)
|
|
_track_steam_distance(abs(current_speed) * delta)
|
|
|
|
if enable_stops and stop_offset.size() > 0 and has_valid_stop:
|
|
var target_offset = stop_offset[next_stop_index]
|
|
var exceeded_forward = (current_speed > 0 and last_progress < target_offset and train_progress >= target_offset)
|
|
var exceeded_back = (current_speed < 0 and last_progress > target_offset and train_progress <= target_offset)
|
|
|
|
if exceeded_forward or exceeded_back:
|
|
train_progress = target_offset
|
|
_execute_stop(current_speed > 0)
|
|
|
|
if wrapped_forward:
|
|
if stop_offset.size() > 0: next_stop_index = 0
|
|
elif wrapped_back:
|
|
if stop_offset.size() > 0: next_stop_index = stop_offset.size() - 1
|
|
|
|
var center_position = to_global(curve.sample_baked(train_progress, true))
|
|
var prog_back = wrapf(train_progress - 2.0, 0.0, total_length)
|
|
var back_position = to_global(curve.sample_baked(prog_back, true))
|
|
var prog_forward = wrapf(train_progress + 2.0, 0.0, total_length)
|
|
var forward_position = to_global(curve.sample_baked(prog_forward, true))
|
|
|
|
if center_position.distance_to(forward_position) > 0.01:
|
|
train_instance.global_position = center_position
|
|
train_instance.look_at(forward_position, Vector3.UP)
|
|
train_instance.rotate_y(PI)
|
|
|
|
if cameras:
|
|
cameras.global_position = center_position
|
|
cameras.global_basis = train_instance.global_basis
|
|
|
|
_snap_wagons_to_progress(total_length)
|
|
|
|
var real_speed = abs(train_speed * stop_multiply)
|
|
var speed_factor = clamp(real_speed / max(speed_max, 0.001), 0.0, 1.0)
|
|
var dir_prev = (center_position - back_position).normalized()
|
|
var dir_next = (forward_position - center_position).normalized()
|
|
var signed_curve = dir_prev.cross(dir_next).y
|
|
var curve_amount = clamp(abs(signed_curve) * 8.0, 0.0, 1.0)
|
|
|
|
curve_roll = lerp(curve_roll, (-signed_curve * 0.08) * speed_factor, delta * 4.0)
|
|
curve_pitch = lerp(curve_pitch, curve_amount * 0.012 * speed_factor, delta * 4.0)
|
|
|
|
if not stop_ongoing:
|
|
swing_time += delta * real_speed * (2.0 + curve_amount)
|
|
var amplitude_z = 0.006 * basic_swing
|
|
var amplitude_x = 0.004 * basic_swing
|
|
var curve_sway = sin(swing_time * 1.35) * 0.01 * curve_amount * speed_factor
|
|
train_instance.rotation.z += sin(swing_time) * amplitude_z
|
|
train_instance.rotation.x += cos(swing_time * 0.8) * amplitude_x
|
|
train_instance.rotation.z += curve_roll + curve_sway
|
|
train_instance.rotation.x += curve_pitch
|
|
else:
|
|
train_instance.rotation.z += curve_roll
|
|
train_instance.rotation.x += curve_pitch
|
|
|
|
#Calculate train distance for steam stats
|
|
func _track_steam_distance(distance_units: float) -> void:
|
|
if distance_units <= 0.0:
|
|
return
|
|
|
|
# Always track the global distance in our save file (for UI and persistency)
|
|
GameState.save_data.total_distance_km += distance_units * DISTANCE_KM_PER_UNIT
|
|
StatsManager.add_distance_km(distance_units * DISTANCE_KM_PER_UNIT)
|
|
|
|
func _execute_stop(go_forward: bool = true) -> void:
|
|
if not _find_next_valid_stop(go_forward):
|
|
stop_multiply = 1.0
|
|
return
|
|
|
|
stop_ongoing = true
|
|
stop_multiply = 0.0
|
|
|
|
var current_stop_index = next_stop_index
|
|
StatsManager.add_int("stat_stop_number", 1)
|
|
StatsManager.store()
|
|
|
|
_advance_next_stop_index(go_forward)
|
|
|
|
if fireworks_scene != null:
|
|
var marker_position = stops_position[current_stop_index]
|
|
var firework_number = randi_range(5, 8)
|
|
for i in range(firework_number):
|
|
var fire_root = fireworks_scene.instantiate()
|
|
get_tree().current_scene.add_child(fire_root)
|
|
|
|
var offset_x = randf_range(-5.0, 5.0)
|
|
var offset_z = randf_range(-5.0, 5.0)
|
|
|
|
fire_root.global_position = marker_position + Vector3(offset_x, 0.5, offset_z)
|
|
|
|
if fire_root.has_method("set_color"):
|
|
fire_root.set_color(fireworks_colors.pick_random())
|
|
if fire_root.has_method("turn_on"):
|
|
fire_root.turn_on()
|
|
|
|
await get_tree().create_timer(randf_range(0.3, 0.8)).timeout
|
|
|
|
await get_tree().create_timer(stop_time).timeout
|
|
|
|
stop_ongoing = false
|
|
is_restarting = true
|
|
|
|
if tween_restart and tween_restart.is_valid():
|
|
tween_restart.kill()
|
|
|
|
tween_restart = create_tween()
|
|
tween_restart.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
|
tween_restart.tween_property(self, "stop_multiply", 1.0, restart_time)
|
|
tween_restart.tween_callback(func(): is_restarting = false)
|
|
|
|
func build_train() -> void:
|
|
if train_model != null:
|
|
train_instance = train_model.instantiate()
|
|
add_child(train_instance)
|
|
else:
|
|
build_default_train()
|
|
|
|
var wagons = wagon_count
|
|
if wagon_random_number:
|
|
wagons = randi_range(1, wagon_count)
|
|
build_train_wagons(wagons)
|
|
|
|
func build_train_wagons(wagon_number: int) -> void:
|
|
_clear_train_wagons()
|
|
if wagon_number <= 0 or wagon_pool == null or not "available_wagons" in wagon_pool:
|
|
return
|
|
if wagon_pool.available_wagons.is_empty():
|
|
return
|
|
|
|
for i in range(wagon_number):
|
|
var wagon_scene: PackedScene = wagon_pool.available_wagons.pick_random()
|
|
if wagon_scene == null:
|
|
continue
|
|
|
|
var wagon: Node3D = wagon_scene.instantiate() as Node3D
|
|
if wagon == null:
|
|
push_warning("Wagon scene root must be a Node3D.")
|
|
continue
|
|
|
|
add_child(wagon)
|
|
wagon_instances.append(wagon)
|
|
|
|
_update_wagon_progress_offsets()
|
|
_snap_wagons_to_progress()
|
|
|
|
func build_default_train() -> void:
|
|
train_instance = Node3D.new()
|
|
add_child(train_instance)
|
|
|
|
var mat_body = StandardMaterial3D.new()
|
|
mat_body.albedo_color = Color(0.8, 0.15, 0.15)
|
|
var mat_glass = StandardMaterial3D.new()
|
|
mat_glass.albedo_color = Color(0.2, 0.8, 1.0)
|
|
|
|
var train_base = MeshInstance3D.new()
|
|
var mesh_base = BoxMesh.new()
|
|
mesh_base.size = Vector3(1.4, 0.8, 3.0)
|
|
train_base.mesh = mesh_base
|
|
train_base.material_override = mat_body
|
|
train_base.position = Vector3(0, 0.6, 0)
|
|
train_instance.add_child(train_base)
|
|
|
|
var cabin = MeshInstance3D.new()
|
|
var mesh_cabin = BoxMesh.new()
|
|
mesh_cabin.size = Vector3(1.4, 1.0, 1.2)
|
|
cabin.mesh = mesh_cabin
|
|
cabin.material_override = mat_glass
|
|
cabin.position = Vector3(0, 1.5, -0.8)
|
|
train_instance.add_child(cabin)
|
|
|
|
var stack = MeshInstance3D.new()
|
|
var mesh_stack = CylinderMesh.new()
|
|
mesh_stack.top_radius = 0.2
|
|
mesh_stack.bottom_radius = 0.3
|
|
mesh_stack.height = 0.8
|
|
stack.mesh = mesh_stack
|
|
stack.material_override = mat_body
|
|
stack.position = Vector3(0, 1.2, 1.0)
|
|
train_instance.add_child(stack)
|
|
|
|
func _clear_train_wagons() -> void:
|
|
for wagon in wagon_instances:
|
|
if is_instance_valid(wagon):
|
|
wagon.queue_free()
|
|
wagon_instances.clear()
|
|
wagon_progress_offsets.clear()
|
|
|
|
func _update_wagon_progress_offsets() -> void:
|
|
wagon_progress_offsets.clear()
|
|
if train_instance == null or wagon_instances.is_empty():
|
|
return
|
|
|
|
var train_length: float = _get_vehicle_length(train_instance)
|
|
var previous_length: float = train_length
|
|
var accumulated_distance: float = 0.0
|
|
|
|
for wagon in wagon_instances:
|
|
var wagon_length: float = _get_vehicle_length(wagon)
|
|
if wagon_spacing_override > 0.0:
|
|
accumulated_distance += wagon_spacing_override + wagon_gap
|
|
else:
|
|
var detected_spacing: float = previous_length * 0.5 + wagon_length * 0.5
|
|
accumulated_distance += detected_spacing * wagon_spacing_scale + wagon_gap
|
|
|
|
wagon_progress_offsets.append(accumulated_distance)
|
|
previous_length = wagon_length
|
|
|
|
func _snap_wagons_to_progress(total_length: float = 0.0) -> void:
|
|
if curve == null or wagon_instances.is_empty():
|
|
return
|
|
|
|
if total_length <= 0.0:
|
|
total_length = curve.get_baked_length()
|
|
if total_length <= 0.0:
|
|
return
|
|
|
|
var previous_progress: float = wrapf(train_progress, 0.0, total_length)
|
|
var previous_position: Vector3 = to_global(curve.sample_baked(previous_progress, true))
|
|
var previous_wagon_offset: float = 0.0
|
|
|
|
for i in range(wagon_instances.size()):
|
|
var wagon: Node3D = wagon_instances[i]
|
|
if not is_instance_valid(wagon):
|
|
continue
|
|
|
|
var wagon_offset: float = float(i + 1) * 7.0
|
|
if i < wagon_progress_offsets.size():
|
|
wagon_offset = wagon_progress_offsets[i]
|
|
|
|
var wagon_progress: float = train_progress - wagon_offset
|
|
var segment_spacing: float = maxf(wagon_offset - previous_wagon_offset, 0.1)
|
|
|
|
var vehicle_progress: float = wrapf(wagon_progress, 0.0, total_length)
|
|
var center_position: Vector3 = to_global(curve.sample_baked(vehicle_progress, true))
|
|
var zero_spacing_fix_weight: float = _get_zero_spacing_fix_weight(previous_progress, vehicle_progress, total_length)
|
|
if zero_spacing_fix_weight > 0.0:
|
|
var target_distance: float = _get_zero_spacing_target_distance(segment_spacing)
|
|
var corrected_progress: float = _find_zero_spacing_progress(previous_progress, previous_position, segment_spacing, target_distance, total_length)
|
|
vehicle_progress = _lerp_progress_on_track(vehicle_progress, corrected_progress, zero_spacing_fix_weight, total_length)
|
|
center_position = to_global(curve.sample_baked(vehicle_progress, true))
|
|
|
|
var prog_forward: float = wrapf(vehicle_progress + 2.0, 0.0, total_length)
|
|
var forward_position: Vector3 = to_global(curve.sample_baked(prog_forward, true))
|
|
|
|
wagon.global_position = center_position
|
|
if center_position.distance_to(forward_position) > 0.01:
|
|
wagon.look_at(forward_position, Vector3.UP)
|
|
wagon.rotate_y(PI)
|
|
|
|
previous_progress = vehicle_progress
|
|
previous_position = center_position
|
|
previous_wagon_offset = wagon_offset
|
|
|
|
func _get_zero_spacing_fix_weight(previous_progress: float, vehicle_progress: float, total_length: float) -> float:
|
|
var distance_to_zero: float = minf(
|
|
_get_progress_distance_to_zero(previous_progress, total_length),
|
|
_get_progress_distance_to_zero(vehicle_progress, total_length)
|
|
)
|
|
var full_fix_distance: float = maxf(TRACK_ZERO_SPACING_FIX_DISTANCE - TRACK_ZERO_SPACING_BLEND_DISTANCE, 0.0)
|
|
return 1.0 - smoothstep(full_fix_distance, TRACK_ZERO_SPACING_FIX_DISTANCE, distance_to_zero)
|
|
|
|
func _get_progress_distance_to_zero(progress: float, total_length: float) -> float:
|
|
var wrapped_progress: float = wrapf(progress, 0.0, total_length)
|
|
return minf(wrapped_progress, total_length - wrapped_progress)
|
|
|
|
func _get_zero_spacing_target_distance(segment_spacing: float) -> float:
|
|
return minf(maxf(segment_spacing * TRACK_ZERO_SPACING_TARGET_RATIO, TRACK_ZERO_SPACING_MIN_DISTANCE), segment_spacing)
|
|
|
|
func _find_zero_spacing_progress(previous_progress: float, previous_position: Vector3, segment_spacing: float, minimum_distance: float, total_length: float) -> float:
|
|
var start_distance: float = minf(minimum_distance, segment_spacing)
|
|
var best_progress: float = wrapf(previous_progress - start_distance, 0.0, total_length)
|
|
var search_limit: float = minf(maxf(segment_spacing, minimum_distance) + TRACK_ZERO_SPACING_SEARCH_EXTRA, total_length)
|
|
var search_distance: float = start_distance
|
|
var previous_search_distance: float = start_distance
|
|
|
|
while search_distance <= search_limit:
|
|
var candidate_progress: float = wrapf(previous_progress - search_distance, 0.0, total_length)
|
|
var candidate_position: Vector3 = to_global(curve.sample_baked(candidate_progress, true))
|
|
best_progress = candidate_progress
|
|
if previous_position.distance_to(candidate_position) >= minimum_distance:
|
|
if is_equal_approx(search_distance, start_distance):
|
|
return candidate_progress
|
|
|
|
return _refine_zero_spacing_progress(
|
|
previous_progress,
|
|
previous_position,
|
|
previous_search_distance,
|
|
search_distance,
|
|
minimum_distance,
|
|
total_length
|
|
)
|
|
|
|
previous_search_distance = search_distance
|
|
search_distance += TRACK_ZERO_SPACING_SEARCH_STEP
|
|
|
|
return best_progress
|
|
|
|
func _refine_zero_spacing_progress(previous_progress: float, previous_position: Vector3, min_search_distance: float, max_search_distance: float, minimum_distance: float, total_length: float) -> float:
|
|
var low_distance: float = min_search_distance
|
|
var high_distance: float = max_search_distance
|
|
|
|
for i in range(8):
|
|
var mid_distance: float = (low_distance + high_distance) * 0.5
|
|
var mid_progress: float = wrapf(previous_progress - mid_distance, 0.0, total_length)
|
|
var mid_position: Vector3 = to_global(curve.sample_baked(mid_progress, true))
|
|
if previous_position.distance_to(mid_position) >= minimum_distance:
|
|
high_distance = mid_distance
|
|
else:
|
|
low_distance = mid_distance
|
|
|
|
return wrapf(previous_progress - high_distance, 0.0, total_length)
|
|
|
|
func _lerp_progress_on_track(from_progress: float, to_progress: float, weight: float, total_length: float) -> float:
|
|
var wrapped_delta: float = wrapf(to_progress - from_progress + total_length * 0.5, 0.0, total_length) - total_length * 0.5
|
|
return wrapf(from_progress + wrapped_delta * clampf(weight, 0.0, 1.0), 0.0, total_length)
|
|
|
|
func _get_vehicle_length(vehicle: Node3D) -> float:
|
|
var bounds: AABB = _get_node_local_bounds(vehicle, vehicle)
|
|
if bounds.size == Vector3.ZERO:
|
|
return 7.0
|
|
|
|
return maxf(bounds.size.z, 0.1)
|
|
|
|
func _get_node_local_bounds(root: Node3D, node: Node) -> AABB:
|
|
var result: AABB = AABB()
|
|
var has_bounds: bool = false
|
|
|
|
var mesh_instance := node as MeshInstance3D
|
|
if mesh_instance != null and mesh_instance.mesh != null:
|
|
var mesh_bounds: AABB = mesh_instance.get_aabb()
|
|
var mesh_transform: Transform3D = root.global_transform.affine_inverse() * mesh_instance.global_transform
|
|
for corner in _get_aabb_corners(mesh_bounds):
|
|
var local_point: Vector3 = mesh_transform * corner
|
|
if has_bounds:
|
|
result = result.expand(local_point)
|
|
else:
|
|
result = AABB(local_point, Vector3.ZERO)
|
|
has_bounds = true
|
|
|
|
for child in node.get_children():
|
|
var child_bounds: AABB = _get_node_local_bounds(root, child)
|
|
if child_bounds.size == Vector3.ZERO:
|
|
continue
|
|
|
|
if has_bounds:
|
|
result = result.merge(child_bounds)
|
|
else:
|
|
result = child_bounds
|
|
has_bounds = true
|
|
|
|
return result
|
|
|
|
func _get_aabb_corners(bounds: AABB) -> Array[Vector3]:
|
|
var start: Vector3 = bounds.position
|
|
var end: Vector3 = bounds.end
|
|
return [
|
|
Vector3(start.x, start.y, start.z),
|
|
Vector3(end.x, start.y, start.z),
|
|
Vector3(start.x, end.y, start.z),
|
|
Vector3(end.x, end.y, start.z),
|
|
Vector3(start.x, start.y, end.z),
|
|
Vector3(end.x, start.y, end.z),
|
|
Vector3(start.x, end.y, end.z),
|
|
Vector3(end.x, end.y, end.z),
|
|
]
|