diff --git a/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md b/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md index c1ee6e2..12f1733 100644 --- a/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md +++ b/docs/VERTICAL_SLICE_TECHNICAL_PLAN.md @@ -807,6 +807,11 @@ jauges analogiques fonctionnelles, des matériaux métalliques patinés et un éclairage pratique chaud/froid. La lisibilité du hublot et des instruments a été revue nativement en Forward+ à 1280 × 720. +Complément du 3 octobre 2026 : le plafond, le boîtier supérieur et les faces de +jauges utilisent des textures bitmap dédiées. Les cadrans reçoivent en plus des +graduations, une aiguille peinte et une vitre distincte ; le plafonnier est +réduit et protégé par une grille pour conserver des proportions crédibles. + - Construire une coque intérieure 3D fixe par rapport au véhicule, indépendante du regard de la caméra. - Remplacer le cercle Canvas par un hublot 3D occultant avec cadre, joint, verre, diff --git a/game/presentation/cockpit/assets/README.md b/game/presentation/cockpit/assets/README.md new file mode 100644 index 0000000..4bf2589 --- /dev/null +++ b/game/presentation/cockpit/assets/README.md @@ -0,0 +1,22 @@ +# Cockpit texture provenance + +These textures were generated specifically for Ransom's Wake on 3 October 2026 +with OpenAI's built-in image generation tool. No third-party image was used as +an input or reference. + +## `naval_painted_steel_albedo.png` + +Prompt summary: seamless, orthographic PBR-style albedo of dark blue-green +naval painted steel, with restrained salt-air oxidation, micro-scratches, +paint chips and grime; neutral illumination; no borders, rivets, text or +watermark. + +## `analog_gauge_face_albedo.png` + +Prompt summary: orthographic vintage naval gauge face in aged ivory enamel, +with fine crazing, soot, speckling and a faint printed arc; no bezel, needle, +hub, glass, text, numbers, logo or watermark. + +Both source outputs were resized to 1024 × 1024 and stripped of metadata for +runtime use. The original generated outputs remain in the local Codex image +generation store and are not required by the game. diff --git a/game/presentation/cockpit/assets/analog_gauge_face_albedo.png b/game/presentation/cockpit/assets/analog_gauge_face_albedo.png new file mode 100644 index 0000000..0b69e18 Binary files /dev/null and b/game/presentation/cockpit/assets/analog_gauge_face_albedo.png differ diff --git a/game/presentation/cockpit/assets/naval_painted_steel_albedo.png b/game/presentation/cockpit/assets/naval_painted_steel_albedo.png new file mode 100644 index 0000000..9d7ab98 Binary files /dev/null and b/game/presentation/cockpit/assets/naval_painted_steel_albedo.png differ diff --git a/game/presentation/cockpit/cockpit_analog_gauges.gd b/game/presentation/cockpit/cockpit_analog_gauges.gd index 8f01501..47de666 100644 --- a/game/presentation/cockpit/cockpit_analog_gauges.gd +++ b/game/presentation/cockpit/cockpit_analog_gauges.gd @@ -12,7 +12,7 @@ var _pressure_needle: Node3D func _init(materials: CockpitMaterialPalette) -> void: _materials = materials name = "AnalogGauges" - _battery_needle = _add_gauge("BatteryGauge", Vector3(-0.34, 0.48, -1.14), "V") + _battery_needle = _add_gauge("BatteryGauge", Vector3(-0.34, 0.48, -1.14), "BAT") _pressure_needle = _add_gauge("PressureGauge", Vector3(0.34, 0.48, -1.14), "BAR") @@ -70,10 +70,13 @@ func _add_gauge(gauge_name: String, position: Vector3, caption: String) -> Node3 face_mesh.height = 0.039 face_mesh.radial_segments = 32 face.mesh = face_mesh - face.material_override = _materials.ivory + face.material_override = _materials.gauge_face face.rotation_degrees.x = 90.0 face.position.z = 0.003 gauge.add_child(face) + _add_scale_label(gauge, "0", Vector3(-0.052, -0.018, 0.028)) + _add_scale_label(gauge, "50", Vector3(0.0, 0.052, 0.028)) + _add_scale_label(gauge, "100", Vector3(0.052, -0.018, 0.028)) var needle_root := Node3D.new() needle_root.name = "Needle" needle_root.position.z = 0.027 @@ -81,9 +84,9 @@ func _add_gauge(gauge_name: String, position: Vector3, caption: String) -> Node3 CockpitMeshFactory.add_box( "NeedleBlade", needle_root, - Vector3(0.0, 0.033, 0.0), - Vector3(0.012, 0.064, 0.008), - _materials.red_emission + Vector3(0.0, 0.034, 0.0), + Vector3(0.008, 0.068, 0.006), + _materials.needle_paint ) CockpitMeshFactory.add_cylinder( "NeedleHub", @@ -100,11 +103,35 @@ func _add_gauge(gauge_name: String, position: Vector3, caption: String) -> Node3 label.font_size = 12 label.pixel_size = 0.0007 label.modulate = Color(0.11, 0.12, 0.10) - label.position = Vector3(0.0, -0.045, 0.032) + label.position = Vector3(0.0, -0.044, 0.032) gauge.add_child(label) + var glass := MeshInstance3D.new() + glass.name = "GaugeGlass" + var glass_mesh := CylinderMesh.new() + glass_mesh.top_radius = 0.081 + glass_mesh.bottom_radius = 0.081 + glass_mesh.height = 0.004 + glass_mesh.radial_segments = 40 + glass.mesh = glass_mesh + glass.material_override = _materials.gauge_glass + glass.rotation_degrees.x = 90.0 + glass.position.z = 0.036 + glass.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + gauge.add_child(glass) return needle_root +func _add_scale_label(gauge: Node3D, text: String, position: Vector3) -> void: + var label := Label3D.new() + label.name = "Scale%s" % text + label.text = text + label.font_size = 9 + label.pixel_size = 0.00048 + label.modulate = Color(0.09, 0.085, 0.065) + label.position = position + gauge.add_child(label) + + func _set_needle_ratio(needle: Node3D, ratio: float) -> void: if needle != null: needle.rotation_degrees.z = lerpf(125.0, -125.0, clampf(ratio, 0.0, 1.0)) diff --git a/game/presentation/cockpit/cockpit_detailing.gd b/game/presentation/cockpit/cockpit_detailing.gd index a9ba5b9..ce023cb 100644 --- a/game/presentation/cockpit/cockpit_detailing.gd +++ b/game/presentation/cockpit/cockpit_detailing.gd @@ -11,6 +11,7 @@ const CockpitMeshFactory = preload("res://game/presentation/cockpit/cockpit_mesh var _materials: CockpitMaterialPalette var _painted_steel: Material var _dark_steel: Material +var _overhead_steel: Material var _brass: Material var _copper: Material var _rubber: Material @@ -65,10 +66,19 @@ func battery_needle_degrees() -> float: return 0.0 if _analog_gauges == null else _analog_gauges.battery_needle_degrees() +func textures_are_loaded() -> bool: + return ( + _materials != null + and _materials.overhead_steel.albedo_texture != null + and _materials.gauge_face.albedo_texture != null + ) + + func _create_materials() -> void: _materials = CockpitMaterialPalette.new() _painted_steel = _materials.painted_steel _dark_steel = _materials.dark_steel + _overhead_steel = _materials.overhead_steel _brass = _materials.brass _copper = _materials.copper _rubber = _materials.rubber @@ -89,16 +99,21 @@ func _apply_materials_to_existing_cockpit() -> void: _apply_material_to_paths( _dark_steel, [ - "CabinShell/Ceiling", "CabinShell/RearBulkhead", "PortholeAssembly/LeftBulkhead", "PortholeAssembly/RightBulkhead", - "PortholeAssembly/TopBulkhead", "PortholeAssembly/BottomBulkhead", "ThrottleLever", "VerticalLever", ] ) + _apply_material_to_paths( + _overhead_steel, + [ + "CabinShell/Ceiling", + "PortholeAssembly/TopBulkhead", + ] + ) _apply_material_to_paths( _brass, [ @@ -216,27 +231,37 @@ func _build_overhead_console() -> void: CockpitMeshFactory.add_box( "BreakerHousing", root, - Vector3(0.0, 0.90, -1.08), - Vector3(0.66, 0.13, 0.27), - _painted_steel, + Vector3(0.0, 0.94, -1.06), + Vector3(0.30, 0.09, 0.12), + _overhead_steel, Vector3(-8.0, 0.0, 0.0) ) CockpitMeshFactory.add_box( "WarmLightDiffuser", root, - Vector3(0.0, 0.835, -0.89), - Vector3(0.38, 0.018, 0.065), + Vector3(0.0, 0.895, -0.985), + Vector3(0.13, 0.010, 0.026), _warm_emission, Vector3(-8.0, 0.0, 0.0) ) - for switch_index in 7: - var x := -0.24 + switch_index * 0.08 + for guard_index in 5: + var guard_x := -0.05 + guard_index * 0.025 + CockpitMeshFactory.add_box( + "LampGuard", + root, + Vector3(guard_x, 0.896, -0.968), + Vector3(0.005, 0.045, 0.008), + _dark_steel, + Vector3(-8.0, 0.0, 0.0) + ) + for switch_index in 5: + var x := -0.10 + switch_index * 0.05 CockpitMeshFactory.add_cylinder( "BreakerSwitch", root, - Vector3(x, 0.83, -0.88), - 0.006, - 0.035, + Vector3(x, 0.965, -0.985), + 0.004, + 0.018, _rubber, Vector3(90.0, 0.0, 0.0) ) diff --git a/game/presentation/cockpit/cockpit_material_palette.gd b/game/presentation/cockpit/cockpit_material_palette.gd index 6853dd4..b6a40eb 100644 --- a/game/presentation/cockpit/cockpit_material_palette.gd +++ b/game/presentation/cockpit/cockpit_material_palette.gd @@ -1,14 +1,23 @@ class_name CockpitMaterialPalette extends RefCounted +const NAVAL_STEEL_TEXTURE_PATH := ( + "res://game/presentation/cockpit/assets/naval_painted_steel_albedo.png" +) +const GAUGE_FACE_TEXTURE_PATH := ( + "res://game/presentation/cockpit/assets/analog_gauge_face_albedo.png" +) + var painted_steel: Material var dark_steel: Material +var overhead_steel: StandardMaterial3D var brass: Material var copper: Material var rubber: Material -var ivory: Material +var gauge_face: StandardMaterial3D +var gauge_glass: StandardMaterial3D +var needle_paint: StandardMaterial3D var warm_emission: StandardMaterial3D -var red_emission: StandardMaterial3D func _init() -> void: @@ -26,6 +35,13 @@ func _init() -> void: 0.44, 0.08 ) + overhead_steel = _textured_material( + _load_texture(NAVAL_STEEL_TEXTURE_PATH), + Color(0.64, 0.69, 0.68), + 0.64, + 0.56, + Vector3(1.6, 1.6, 1.6) + ) brass = _weathered_material( Color(0.28, 0.18, 0.045), Color(0.08, 0.16, 0.13), @@ -41,9 +57,15 @@ func _init() -> void: 0.12 ) rubber = _plain_material(Color(0.012, 0.018, 0.018), 0.0, 0.92) - ivory = _plain_material(Color(0.72, 0.69, 0.56), 0.05, 0.63) + gauge_face = _textured_material( + _load_texture(GAUGE_FACE_TEXTURE_PATH), + Color(0.78, 0.74, 0.64), + 0.02, + 0.72 + ) + gauge_glass = _glass_material() + needle_paint = _plain_material(Color(0.68, 0.055, 0.022), 0.12, 0.44) warm_emission = _emissive_material(Color(0.96, 0.52, 0.18), 2.4) - red_emission = _emissive_material(Color(0.92, 0.055, 0.025), 3.6) func _weathered_material( @@ -110,6 +132,41 @@ func _plain_material( return material +func _textured_material( + texture: Texture2D, + tint: Color, + metallic: float, + roughness: float, + uv_scale: Vector3 = Vector3.ONE +) -> StandardMaterial3D: + var material := StandardMaterial3D.new() + material.albedo_color = tint + material.albedo_texture = texture + material.metallic = metallic + material.roughness = roughness + material.uv1_scale = uv_scale + material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC + return material + + +func _load_texture(resource_path: String) -> ImageTexture: + var image := Image.load_from_file(resource_path) + if image == null or image.is_empty(): + push_error("Unable to load cockpit texture: %s" % resource_path) + return null + return ImageTexture.create_from_image(image) + + +func _glass_material() -> StandardMaterial3D: + var material := StandardMaterial3D.new() + material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + material.albedo_color = Color(0.42, 0.68, 0.66, 0.13) + material.metallic = 0.12 + material.roughness = 0.08 + material.cull_mode = BaseMaterial3D.CULL_DISABLED + return material + + func _emissive_material(color: Color, energy: float) -> StandardMaterial3D: var material := StandardMaterial3D.new() material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED diff --git a/tests/integration/run_vertical_slice_foundation.gd b/tests/integration/run_vertical_slice_foundation.gd index 7f1595f..e7a40b7 100644 --- a/tests/integration/run_vertical_slice_foundation.gd +++ b/tests/integration/run_vertical_slice_foundation.gd @@ -16,7 +16,7 @@ const ProductionInputAdapter = preload( "res://game/infrastructure/input/production_input_adapter.gd" ) -const TEST_COUNT := 55 +const TEST_COUNT := 56 var _failures: Array[String] = [] @@ -111,6 +111,7 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void: _expect(detailing.pressure_hull_rib_count() >= 6, "cockpit exposes reinforced pressure-hull structure") _expect(detailing.pipe_segment_count() >= 13, "cockpit exposes a complete life-support pipe run") _expect(absf(detailing.battery_needle_degrees()) > 100.0, "analog battery gauge follows the dive snapshot") + _expect(detailing.textures_are_loaded(), "cockpit loads its overhead and analog gauge textures") _expect("kWh" in instruments.resources_text() and "kW" in instruments.resources_text(), "battery instrument has exact units") _expect("L/min" in instruments.resources_text(), "oxygen instrument has volume and consumption") _expect("bar" in instruments.integrity_text(), "hull instrument has current and certified pressure")