Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f9b7c0894 |
No files matched your search
@@ -3,3 +3,6 @@
|
||||
*.tmp
|
||||
*.log
|
||||
builds/
|
||||
|
||||
# Textures que Godot extrait des .glb a l import : regenerees, non versionnees
|
||||
game/presentation/cockpit/assets/models/*.png
|
||||
|
After Width: | Height: | Size: 920 KiB |
|
After Width: | Height: | Size: 1005 KiB |
|
After Width: | Height: | Size: 961 KiB |
|
After Width: | Height: | Size: 131 KiB |
@@ -0,0 +1,384 @@
|
||||
# Brief — intégrer les cinq modèles du cockpit
|
||||
|
||||
**Statut : exécuté.** Ce brief a servi à l'intégration des cinq premiers modèles ; sept autres (hublot, coque, lampe, bouteille, mains courantes, console) ont suivi et la cabine a été reconstruite autour. Il reste comme trace des décisions (section 10) ; l'état réel est celui du code et de `assets/README.md`.
|
||||
|
||||
Rédigé le 2026-10-06 à partir d'une lecture du dépôt (code non exécuté, barrière non lancée) et des
|
||||
modèles produits dans une session Blender séparée. Tout ce qui est marqué « non vérifié » doit être
|
||||
vérifié avant d'être cru.
|
||||
|
||||
---
|
||||
|
||||
## 1. Objectif
|
||||
|
||||
Le cockpit de production (`game/presentation/cockpit/`) fabrique aujourd'hui tous ses instruments en
|
||||
code, à partir de primitives. Cinq modèles 3D texturés existent maintenant pour les instruments
|
||||
principaux. Le travail : les importer, les placer dans `cockpit_interior.tscn` à la place de la géométrie
|
||||
procédurale qu'ils remplacent, et les brancher sur les mêmes données qu'aujourd'hui, sans déplacer une
|
||||
seule règle de jeu.
|
||||
|
||||
Ce n'est pas une refonte du cockpit. Coque, hublot, équipements latéraux, tuyauterie, lampes en cage,
|
||||
consoles, leviers et HUD restent tels quels.
|
||||
|
||||
Terminé quand :
|
||||
- les cinq modèles sont dans la scène, à l'échelle, orientés vers la caméra du pilote ;
|
||||
- les quatre aiguilles, les voyants d'alerte et l'écran du sonar affichent les mêmes données qu'avant ;
|
||||
- `bash scripts/quality_gate.sh` passe, sans contrôle affaibli ;
|
||||
- des captures à 1280 × 720 et 1920 × 1080 sont jointes, à côté de
|
||||
`docs/visual_targets/cockpit-objective-v1.jpg`.
|
||||
|
||||
## 2. À lire avant de toucher au code
|
||||
|
||||
Obligatoire par `AGENTS.md` : `docs/ENGINEERING_GUARDRAILS.md`, puis :
|
||||
- `docs/adr/0004-forward-plus-only.md` — matériaux PBR, post-traitement en réglage de qualité ;
|
||||
- `docs/adr/0006-sonar-contract-and-acquisition-gate.md` — une seule texture sonar, un seul renderer ;
|
||||
- `docs/adr/0008-composition-owned-service-lifetimes.md` — la composition câble, la présentation affiche ;
|
||||
- `docs/visual_targets/README.md` et l'image cible ;
|
||||
- `docs/VERTICAL_SLICE_TECHNICAL_PLAN.md` L823-842 — hiérarchie visuelle du cockpit, ce qui relève de la
|
||||
tranche 6 ;
|
||||
- `game/presentation/cockpit/assets/README.md` — registre de provenance, à compléter.
|
||||
|
||||
## 3. Les cinq modèles
|
||||
|
||||
### 3.1 Où ils sont
|
||||
|
||||
Copiés le 2026-10-06 dans `game/presentation/cockpit/assets/models/`, **non suivis par git** (décision 8 :
|
||||
ne pas les commiter sans l'accord d'Olivier). Jamais ouverts dans Godot.
|
||||
|
||||
| Fichier | Poids | Pièce |
|
||||
|---|---|---|
|
||||
| `gauge_panel.glb` | ~20 Mo | tableau de jauges |
|
||||
| `sonar.glb` | ~15 Mo | écran sonar |
|
||||
| `switch_panel.glb` | ~16 Mo | panneau d'interrupteurs |
|
||||
| `alert_box.glb` | ~4,5 Mo (atlas 1024²) | boîtier d'alerte à trois voyants |
|
||||
| `speaker.glb` | ~4,5 Mo (atlas 1024²) | boîtier haut-parleur |
|
||||
|
||||
Les sources (scripts Blender, `.blend`) restent dans un dossier temporaire de session, hors dépôt :
|
||||
`C:\Users\olivi\AppData\Local\Temp\claude\C--Users-olivi-Projects-Ares-Shift--claude-worktrees-blender-game-piece-c61349\7aedfdfa-be9c-40ed-8dc7-d4243ff07bc3\scratchpad\`.
|
||||
Ne pas les modifier ; une retouche de modèle se demande à Olivier.
|
||||
|
||||
Environ 60 Mo au total, presque entièrement des textures PNG embarquées, 2048² sauf mention (couleur, ORM, normales,
|
||||
parfois émission). **Les textures bougent encore** : des passes d'équilibrage sont en cours. Les noms de
|
||||
fichiers, les noms de nœuds et les pivots sont stables. Importer par référence au `.glb`, sans recopier
|
||||
maillages ni matériaux dans la scène, pour qu'un fichier remplacé se mette à jour seul.
|
||||
|
||||
Ce seront les **premiers assets 3D importés du dépôt** : aucun `.glb`, `.gltf`, `.obj` ni `.fbx` n'y existe.
|
||||
|
||||
### 3.2 Conventions communes
|
||||
|
||||
- glTF 2.0, mètres, exporté de Blender avec +Y vers le haut. Dans Blender la façade regarde −Y, donc
|
||||
**dans Godot la façade regarde +Z** — même convention que le `Dashboard` actuel. Non vérifié à l'import.
|
||||
- Chaque modèle a une racine maillée et, selon les cas, des enfants séparés pour ce qui s'anime ou
|
||||
s'allume. Les enfants n'ont ni rotation ni échelle propres, sauf les aiguilles (voir 5.1).
|
||||
- Échelle de création arbitraire, différente par pièce : voir section 4.
|
||||
- Matériaux simple face, texture d'occlusion déclarée (canal rouge de l'ORM), tangentes exportées.
|
||||
- Extension glTF utilisée : `KHR_materials_emissive_strength` sur les matériaux émissifs.
|
||||
|
||||
### 3.3 Contrat de chaque modèle
|
||||
|
||||
**Tableau de jauges — `gauge_panel.glb`** (créé à 1,2 m de large, origine au bas-centre de la façade)
|
||||
- `GaugePanel` : boîtier, panneaux, lunettes, vis, interrupteurs décoratifs, barre à deux manchons.
|
||||
- `GaugeDials` : les quatre cadrans imprimés, un matériau, une texture 4096 × 1024.
|
||||
- `Needle_Oxygen`, `Needle_Battery`, `Needle_Hull`, `Needle_Depth` : une aiguille chacune, origine sur
|
||||
l'axe du cadran, deux surfaces (lame, axe). Voir 5.1 pour l'angle.
|
||||
- Six voyants, enfants séparés de `GaugePanel`, chacun avec son matériau émissif `GP_<nom du nœud>` :
|
||||
`Light_OxygenOk` (vert), `Light_OxygenWarn`, `Light_BatteryWarn`, `Light_HullWarn`, `Light_Strip_0`,
|
||||
`Light_Strip_1` (ambre). **Propriété à piloter : `emission_energy_multiplier`**, 0 = éteint (verre
|
||||
sombre coloré), 1 = allumé. **Ne pas masquer un nœud `Light_*`** : les faces du boîtier sous la
|
||||
lentille sont supprimées, on verrait un trou. Éteindre par l'émission seulement.
|
||||
- Le boîtier `Gauges_Body` n'a plus aucune émission. Les cadrans `Gauges_Dials` portent un léger
|
||||
rétro-éclairage (facteur d'émission 0,2 sur leur propre texture), pilotable aussi.
|
||||
|
||||
**Écran sonar — `sonar.glb`** (créé à 1,0 m de large, origine au centre du boîtier)
|
||||
- `Sonar` : boîtier, renfoncement, trois boutons ambre (émission cuite, toujours allumés), deux molettes,
|
||||
deux poignées laiton, presse-étoupe.
|
||||
- `SonarScreen` : la vitre, maillage plat séparé, matériau émissif propre portant une image fixe de
|
||||
démonstration. **UV de 0 à 1 sur l'image visible.** Vitre visible : 0,690 × 0,434 à l'échelle de
|
||||
création, soit un rapport de **1,59**.
|
||||
|
||||
**Panneau d'interrupteurs — `switch_panel.glb`** (créé à 0,7 m de large, origine au bas-centre de la façade,
|
||||
façade inclinée de 27° vers l'arrière)
|
||||
- `SwitchPanel` : boîtier, plaque, cadres. `SwitchLabels` : étiquettes, matériau et texture propres
|
||||
(POMPE 1, POMPE 2, SPOT, ALARME, ECLAIRAGE, VENTILATION, SONAR, NAVIGATION, AUXILIAIRE).
|
||||
- Dix enfants animables, pose de repos à rotation nulle, **rotation autour de X** :
|
||||
|
||||
| Nœuds | Plage (`rotation_degrees.x`) |
|
||||
|---|---|
|
||||
| `Toggle_Eclairage`, `Toggle_Ventilation`, `Toggle_Sonar`, `Toggle_Navigation`, `Toggle_Auxiliaire` | 0 (bas) → −70 (haut) |
|
||||
| `Rocker_Pompe1`, `Rocker_Pompe2`, `Rocker_Spot` | 0 → +16 |
|
||||
| `Cover_Alarme` (capot rouge) | 0 (fermé) → −100 (relevé) |
|
||||
| `Lever_Alarme` | 0 → −100 environ ; traverse le capot s'il est levé capot fermé |
|
||||
|
||||
Plages vérifiées en pose dans Blender, pas dans Godot.
|
||||
|
||||
**Boîtier d'alerte — `alert_box.glb`** (créé à 0,30 m de large hors pattes, origine au bas-centre)
|
||||
- `AlertBox` : boîtier, grille, plaque, étiquettes laiton ALERTE / ATTENTION / DANGER, presse-étoupes.
|
||||
- `Lamp_Alerte` (vert), `Lamp_Attention` (ambre), `Lamp_Danger` (rouge) : une lentille chacune, matériaux
|
||||
`AB_Lamp_Alerte`, `AB_Lamp_Attention`, `AB_Lamp_Danger`, texture d'émission propre.
|
||||
**Propriété à piloter : `emission_energy_multiplier`** du matériau de la lentille. 0 = éteint
|
||||
(la lentille reste lisible en verre coloré), 1,0 = allumé comme conçu.
|
||||
|
||||
**Haut-parleur — `speaker.glb`** (créé à 0,26 m de large hors pattes, origine au bas-centre)
|
||||
- `Speaker`, un nœud, matériau `Speaker_Body`. Rien d'animé, rien d'allumé.
|
||||
|
||||
## 4. Échelle et placement
|
||||
|
||||
**Se caler sur le cockpit existant, pas sur une maquette extérieure.** Le cockpit de production a déjà
|
||||
ses cotes et sa caméra ; les modèles s'y adaptent.
|
||||
|
||||
Repères du cockpit actuel (`cockpit_interior.tscn`, mètres, avant = −Z) :
|
||||
- caméra : `(0, 0.52, −0.48)`, FOV 82° ; regard libre ±30° en lacet, −17° à +11,5° en tangage, plus un
|
||||
retour de mouvement (jusqu'à environ −35° / +29,5° en tangage, ±8,8° en roulis). **Aucune translation.**
|
||||
- `%Dashboard` : `(0, 0.05, −1.22)`, incliné de −12°, dalle de 1,56 × 0,44 × 0,14 ;
|
||||
- bloc d'instruments actuel : 1,42 m de large ; cadrans de 0,203 / 0,203 / 0,186 / 0,242 m de diamètre ;
|
||||
- `SonarMonitor` : `(0.54, 0.59, −1.04)`, rotation `(−5°, −42°, −2°)`, écran actuel de 0,216 × 0,141 m ;
|
||||
- hublot : centre `(0, 0.47, −1.32)`, vitre de 1,12 m.
|
||||
|
||||
Conséquences, à vérifier une fois les modèles importés :
|
||||
- **Tableau de jauges** : ses cadrans font environ 0,21 m (petits) et 0,27 m (profondeur) à l'échelle de
|
||||
création. Il tombe donc presque juste **à l'échelle 1** sur le bloc d'instruments actuel. Le poser sur
|
||||
`%Dashboard`, nom et chemin à décider (voir 7).
|
||||
- **Sonar** : pour retrouver un écran d'environ 0,216 m de large, échelle d'environ **0,31**. Garder la
|
||||
position et l'orientation de `SonarMonitor`, tourné vers la caméra.
|
||||
- **Panneau d'interrupteurs, boîtier d'alerte, haut-parleur** : pas d'équivalent exact aujourd'hui.
|
||||
Tailles réelles plausibles proposées par l'étude d'ensemble : 0,26 m, 0,135 m et 0,145 m de large
|
||||
(échelles 0,37, 0,45 et 0,56). Emplacements d'après l'image cible : interrupteurs en bas à gauche,
|
||||
boîtier d'alerte sur la paroi gauche, haut-parleur sur la paroi droite. Ajuster à l'œil contre la cible.
|
||||
|
||||
**Contrainte de point de vue, importante.** Les modèles ont été faits pour une caméra à position fixe :
|
||||
dos ouverts, faces invisibles supprimées. Ils sont sains tant que la caméra reste dans un cône d'environ
|
||||
35° (40° pour le sonar) autour de la normale de leur façade. La caméra du jeu ne se translate pas, donc
|
||||
une fois chaque pièce tournée vers elle, tourner la tête ne change rien. À faire : après placement,
|
||||
mesurer l'angle entre la normale de chaque façade et la direction pièce → caméra ; au-delà de 30°,
|
||||
réorienter la pièce plutôt que d'accepter le risque. Regarder aussi chaque pièce en tangage extrême.
|
||||
|
||||
Une maquette Blender du cockpit entier existe (`…\scratchpad\capsule\capsule_layout.json`), dérivée de
|
||||
la seule image cible, avec ses propres cotes. Elle ne fait pas foi ici : elle suppose un cockpit plus
|
||||
grand et une caméra différente. Utile seulement comme idée de disposition relative.
|
||||
|
||||
## 5. Brancher les instruments
|
||||
|
||||
Règle du dépôt : la présentation affiche des instantanés, elle ne calcule aucune règle. Le chemin de
|
||||
données ne change pas — `VerticalSliceRoot` appelle `present(update, telemetry)` à chaque pas physique,
|
||||
`CockpitInstrumentPanel` puis `CockpitDetailing` redistribuent des valeurs simples. Seul le dernier
|
||||
maillon change : ce qui tourne et ce qui s'allume.
|
||||
|
||||
### 5.1 Aiguilles
|
||||
|
||||
Aujourd'hui (`cockpit_analog_gauges.gd` L263-265) :
|
||||
`needle.rotation_degrees.z = lerpf(125.0, −125.0, ratio)`, balayage de 250°.
|
||||
|
||||
Les cadrans des nouveaux modèles sont imprimés pour un balayage de **240°** :
|
||||
|
||||
`rotation_degrees.z = 120 − 240 × ratio`
|
||||
|
||||
- ratio 0 → +120° (en bas à gauche), ratio 1 → −120° (en bas à droite), sens horaire vu du pilote.
|
||||
- **Les aiguilles ne sont pas à zéro dans le fichier** : elles sont exportées dans une pose de
|
||||
démonstration (ratios 0,70 / 0,50 / 0,90 / 0,54). Écrire l'angle absolu, ne pas additionner.
|
||||
- Formule vérifiée par lecture du `.glb` et dans Blender. **Signe non vérifié dans Godot** : poser les
|
||||
quatre aiguilles à 0, 0,5 et 1 et regarder où elles pointent avant de conclure.
|
||||
- Les ratios restent ceux choisis dans `CockpitDetailing.present` (L49-60). Ne pas les recalculer ailleurs.
|
||||
|
||||
Graduations imprimées : oxygène, batterie et coque de 0 à 100 %, avec bande rouge 0-17, jaune 17-29,
|
||||
verte 29-100. **Profondeur imprimée de 0 à 100 %** (décision 1), sans bande de couleur, avec un repère
|
||||
rouge juste après 100 : l'aiguille trace `profondeur courante / profondeur certifiée`, comme aujourd'hui.
|
||||
|
||||
### 5.2 Voyants d'alerte
|
||||
|
||||
Aujourd'hui `CockpitAnalogGauges._update_indicator_bank` (L140-150) allume exactement un voyant sur trois
|
||||
selon `snapshot.alerts.maximum` : vert sous ATTENTION, ambre pour ATTENTION et RETURN, rouge pour
|
||||
CRITICAL et FAILURE ; allumé = énergie 2,8, éteint = 0,04.
|
||||
|
||||
Le boîtier d'alerte porte exactement ces trois voyants. Même correspondance, en pilotant
|
||||
`emission_energy_multiplier` de chaque lentille. Qui reprend ce rôle — boîtier d'alerte, voyants du
|
||||
tableau, ou les deux — est la décision 2.
|
||||
|
||||
Rappel du plan de jeu (`GAME_DESIGN_PLAN.md` L258-268) : aucune information critique ne dépend de la
|
||||
seule couleur. Les étiquettes ALERTE / ATTENTION / DANGER et la position des voyants y contribuent ;
|
||||
le panneau d'alerte du HUD reste.
|
||||
|
||||
### 5.3 Écran du sonar
|
||||
|
||||
Aujourd'hui `CockpitInstrumentPanel.attach_sonar_texture` (L85-94) pose sur `%SonarScreen` un
|
||||
`StandardMaterial3D` non éclairé portant la texture du `SubViewport` (960 × 540), en `material_override`.
|
||||
|
||||
Avec le modèle : même méthode, sur le nœud `SonarScreen` importé. Points à respecter :
|
||||
- le test exige que `%SonarScreen` soit un `MeshInstance3D` à nom unique, dont le +Z local est la normale
|
||||
sortante, tourné vers la caméra (voir 7). La vitre du modèle regarde +Z après import ; à vérifier ;
|
||||
- le `material_override` remplace l'image de démonstration embarquée : voulu ;
|
||||
- une seule texture partagée entre vue compacte et vue agrandie (ADR 0006), asserté par le test ;
|
||||
- **rapport d'aspect** : la texture est en 16:9 (1,78), la vitre du modèle en 1,59, l'ancien quad en 1,53.
|
||||
L'image est déjà écrasée d'environ 14 % aujourd'hui. Décision 3.
|
||||
- ne pas ajouter de grille ni de tracé factice par-dessus : le test interdit tout nœud `SonarReferenceTrace`,
|
||||
et l'ancienne grille en boîtes émissives (`SonarGridVertical/Horizontal`) disparaît avec l'ancienne console.
|
||||
|
||||
### 5.4 Interrupteurs
|
||||
|
||||
Purement décoratifs aujourd'hui, et ils le restent dans ce lot : aucune interaction, aucun raycast,
|
||||
aucune action d'entrée n'existe. Les laisser en pose de repos. Les rendre manipulables serait un nouveau
|
||||
périmètre traversant entrées, application et présentation (décision 4).
|
||||
|
||||
### 5.5 Haut-parleur
|
||||
|
||||
Décor. Il n'y a aucun audio dans `game/` aujourd'hui.
|
||||
|
||||
## 6. Rendu : ce qui conditionne l'aspect
|
||||
|
||||
Les modèles ont été réglés et jugés sous une transformation de couleur linéaire (« Standard » de
|
||||
Blender). Bonne nouvelle : le projet est au tonemapper par défaut de Godot, **Linear**, donc proche.
|
||||
Sous une courbe filmique ou AgX, les mêmes modèles sortent pâles et délavés ; ce piège a déjà coûté
|
||||
plusieurs corrections à tort. Ne pas toucher aux textures pour compenser un réglage d'environnement.
|
||||
|
||||
État actuel, relevé dans `game/world/sites/vehicle_test_environment.tscn` : tonemap non réglé (Linear),
|
||||
**glow éteint, SSAO éteint**, brouillard et brouillard volumétrique actifs, ambiance
|
||||
`ambient_light_source = 3` sans ressource Sky. Conséquences :
|
||||
- **Voyants** : sans glow, pas de halo. Les lentilles se lisent par leur seule surface. Les halos de
|
||||
l'image cible n'ont pas été peints dans les textures, exprès. Décision 5.
|
||||
- **Forces d'émission** : réglées sur un éclairage d'essai. À recaler ici, par `emission_energy_multiplier`
|
||||
ou l'énergie d'émission du matériau, pas en retouchant les textures.
|
||||
- **Occlusion** : chaque modèle porte une occlusion cuite, qui n'agit que sur la lumière ambiante, et une
|
||||
crasse de contact cuite dans la couleur de base. Si le SSAO est activé un jour, surveiller le cumul.
|
||||
- **Ambiance** : vérifier à l'image ce que `ambient_light_source = 3` donne réellement sans Sky avant de
|
||||
juger les matériaux.
|
||||
- **Noirs métalliques** : cadres, lunettes, fond de grille et renfoncement de l'écran sont des noirs à
|
||||
`metallic = 1`. C'est voulu : un diélectrique renvoie 4 % et passait au gris sous éclairage.
|
||||
- **Éclairage non peint** : aucun reflet n'est cuit dans les textures (une exception possible sur le
|
||||
boîtier d'alerte, en cours de reprise). Les reflets viennent de la rugosité, du métal et des normales,
|
||||
donc des lampes de `cockpit_practical_lighting.gd`.
|
||||
|
||||
L'environnement appartient à la couche `world`, pas au cockpit. Le modifier sort du périmètre de ce lot
|
||||
sauf décision contraire.
|
||||
|
||||
## 7. Tests : ce qui casse, ce qui doit tenir
|
||||
|
||||
Un seul test touche le cockpit : `tests/integration/run_vertical_slice_foundation.gd`. Il asserte sur des
|
||||
noms, des types et des comptes de nœuds. `AGENTS.md` interdit d'affaiblir un contrôle pour faire passer
|
||||
un changement ; quand la structure change légitimement, le test est **réécrit pour exprimer le même
|
||||
comportement sur la nouvelle structure**, dans le même commit, et le changement est justifié.
|
||||
|
||||
| Ligne | Assertion | Effet du remplacement |
|
||||
|---|---|---|
|
||||
| 93-97, 108 | un nœud existe à `Dashboard/AnalogGauges` | garder ce chemin pour le conteneur des jauges, ou adapter |
|
||||
| 125-127 | `%SonarScreen` est un `MeshInstance3D` | le maillage importé doit porter ce nom unique |
|
||||
| 130, 142-145, 330-343 | `SonarMonitor/Housing` est un `BoxMesh`, écran en avant du boîtier | **casse** : le boîtier importé est un `ArrayMesh`. Réécrire la mesure (bornes du maillage) |
|
||||
| 134-137, 305-314 | `global_basis.z` de l'écran · direction caméra > 0,85 | l'écran doit rester tourné vers la caméra, +Z sortant |
|
||||
| 152 | `abs(battery_needle_degrees()) > 100` au départ | tient avec la nouvelle formule (batterie pleine → −120°) |
|
||||
| 153 | `textures_are_loaded()` : acier + quatre faces de cadran | **casse** si les PNG de faces sont retirés (décision 6) |
|
||||
| 155 | `dashboard_control_count() >= 8` | le nouveau panneau porte 10 commandes |
|
||||
| 158 | `sonar_control_count() >= 4` | le modèle a 3 boutons et 2 molettes : redéfinir ce que la méthode compte |
|
||||
| 162-164 | 4 jauges, 3 indicateurs, exactement 1 actif | dépend de la décision 2 |
|
||||
| 165-167 | textes des afficheurs cachés (kWh, kW, L/min, bar) | les `Label3D` cachés doivent continuer d'être mis à jour |
|
||||
| 169-172 | texture sonar partagée | inchangé |
|
||||
| 150-151, 156-157, 159-161 | couples, tuyaux, équipements latéraux, 2 lampes, cloison | ne pas y toucher |
|
||||
|
||||
`tests/run_load_checks.gd` instancie toute scène sous `game/` : `cockpit_interior.tscn` doit s'instancier
|
||||
seule, modèles compris.
|
||||
|
||||
**La barrière échoue sur toute ligne `ERROR:`, `SCRIPT ERROR:` ou `WARNING:`** dans la sortie des tests.
|
||||
Un glTF qui émet un avertissement au chargement (extension inconnue, texture manquante, tangentes) la
|
||||
fait donc tomber. Contrôler les journaux d'import et de chargement des cinq fichiers **en premier**,
|
||||
avant tout travail de scène. Le script d'export a signalé un avertissement connu côté Blender
|
||||
(« More than one shader node tex image used for a texture ») ; savoir s'il a un équivalent côté Godot
|
||||
n'est pas vérifié.
|
||||
|
||||
Tout comportement nouveau reçoit son test : angle des aiguilles aux ratios 0, 0,5 et 1 ; un seul voyant
|
||||
allumé par niveau d'alerte ; écran tourné vers la caméra.
|
||||
|
||||
## 8. Architecture et conventions
|
||||
|
||||
- Tout ce lot vit dans `game/presentation/cockpit/`. Aucune dépendance vers `world`, `infrastructure` ni
|
||||
`composition` (`check_architecture.sh` L93-99). Rien dans `src/` ni `scenes/`.
|
||||
- Suivre le patron existant : une classe par fichier, `class_name` en PascalCase préfixé `Cockpit`,
|
||||
fichier en snake_case préfixé `cockpit_`, dépendances redéclarées en `const X = preload(...)`, GDScript
|
||||
typé, membres privés en `_`, garde de nullité en tête de chaque `present`, petites méthodes de lecture
|
||||
pour les tests (`gauge_count()`, `indicator_count()`…).
|
||||
- Une seule implémentation par concept : la conversion ratio → angle d'aiguille a **un** propriétaire.
|
||||
En remplaçant `CockpitAnalogGauges`, supprimer l'ancienne, ne pas laisser deux formules.
|
||||
- Supprimer la géométrie procédurale remplacée, ne pas la cacher. Le dépôt cache déjà neuf nœuds
|
||||
d'afficheurs par `hide_obsolete_elements` ; ne pas allonger cette liste.
|
||||
- Ce qui est remplacé : tout `AnalogGauges` (boîtier, quatre jauges, aiguilles, verre, `IndicatorBank`
|
||||
selon la décision 2) ; `SonarMonitor/Housing`, le quad `%SonarScreen`, tout `SonarConsoleDetailing` ;
|
||||
les deux bancs `SwitchPlate` / `ToggleSwitch` de `DashboardDetailing`.
|
||||
- Ce qui reste : coque, hublot et cloison, `SideEquipment` en entier, tuyauterie, couples,
|
||||
`PracticalLighting`, consoles, leviers, cadre structurel du tableau (`TopRail`, `BottomRail`,
|
||||
`SideRail`, `PanelScrew`), `%ReturnLabel`, `%StatusLabel`, `EmergencyDial` (décision 7), HUD.
|
||||
- Vérifier les collisions avec ce qui reste : le cadre structurel et le rail inférieur
|
||||
(`LowerSteelRail`, `LeftGrip`, `RightGrip`) occupent la zone de la barre du nouveau tableau, qui porte
|
||||
déjà ses propres manchons. Probable doublon à retirer ; à juger à l'image.
|
||||
- Fichiers de plus de 350 lignes et fonctions de plus de 40 : seuils de revue du dépôt.
|
||||
|
||||
## 9. Import et dépôt
|
||||
|
||||
- **Emplacement** : `game/presentation/cockpit/assets/models/`, à côté du code qui les utilise, comme
|
||||
les textures actuelles. Noms en snake_case, déjà appliqués (section 3.1).
|
||||
- **Les `.import` ne sont pas versionnés** dans ce dépôt ; la barrière les régénère par `--import`.
|
||||
Tout réglage d'import non par défaut serait donc perdu en CI. S'en tenir aux réglages par défaut et
|
||||
faire toute adaptation dans la scène ou le code, sauf décision de versionner les `.glb.import`.
|
||||
- **60 Mo de binaires** dans un dépôt sans Git LFS ni `.gitattributes`, alors que le plan de jeu nomme
|
||||
LFS pour les gros assets. Décision 8 : pas de LFS, et pas de commit des `.glb` sans accord d'Olivier.
|
||||
Conséquence à signaler dans le compte rendu : tant que les `.glb` ne sont pas commités, la scène ne se
|
||||
charge que sur cette machine ; un clone neuf échouerait à la barrière.
|
||||
- **Budget de textures** (`GAME_DESIGN_PLAN.md` L958-960) : atlas 1K pour les kits courants, 2K réservé
|
||||
au sous-marin et aux éléments héroïques. Le cockpit relève du 2K. Boîtier d'alerte et haut-parleur sont
|
||||
passés en 1024 (aucune différence visible à leur taille d'écran).
|
||||
- **Provenance** : compléter `assets/README.md` dans sa forme actuelle. Les modèles sont générés par
|
||||
scripts Blender écrits pour le projet, d'après des planches de concept générées pour le projet ;
|
||||
aucune ressource tierce. Polices des cadrans et des étiquettes : Arial et Impact, rendues en texture.
|
||||
**Point de licence à vérifier** avant diffusion : ce sont des polices système de Windows.
|
||||
- `project.godot` porte une **modification non commitée** qui supprime `[physics]` et `[rendering]`
|
||||
(ré-enregistrement par l'éditeur, selon toute apparence). Ne pas la commiter par accident avec ce lot.
|
||||
- Ne jamais modifier `.godot/` à la main.
|
||||
|
||||
## 10. Décisions prises
|
||||
|
||||
Tranchées par Olivier le 2026-10-06. Elles priment sur toute formulation conditionnelle des sections
|
||||
précédentes (« voir la décision N », « dépend de la décision N »).
|
||||
|
||||
1. **Cadran de profondeur : pourcentage de la profondeur certifiée.** Le cadran du modèle est regradué de
|
||||
0 à 100 % (texture régénérée par le script du modèle, livrée avec le `.glb`). L'aiguille trace
|
||||
`courante / certifiée`, comme aujourd'hui : aucun changement de domaine. Aucun seuil de jeu n'est
|
||||
imprimé sur le cadran au-delà du repère de fin de course.
|
||||
2. **L'alerte SYS / ATTN / ALRM est portée par le boîtier d'alerte** : `Lamp_Alerte` (verte) = SYS,
|
||||
`Lamp_Attention` (ambre) = ATTN, `Lamp_Danger` (rouge) = ALRM, exactement une allumée. Les six voyants
|
||||
`Light_*` du tableau de jauges ne portent pas ce rôle ; faute de donnée par instrument dans le
|
||||
snapshot, ils restent dans un état fixe de repos (à proposer, pas à inventer une règle).
|
||||
3. **Sonar : le `SubViewport` passe au rapport de la vitre** (1,59 ; 960 × 604). Vérifier que le rendu
|
||||
sonar ne suppose pas 16:9 ; adapter le test qui lit sa taille s'il y en a un.
|
||||
4. **Interrupteurs décoratifs.** Posés au repos (rotations nulles). Aucune entrée, aucune commande.
|
||||
5. **Glow activé, discret.** Réglage d'environnement traité en réglage de qualité selon l'ADR 0004. Seuil
|
||||
réglé pour que seuls voyants et écran sonar débordent, pas le papier des cadrans. Livrer une capture
|
||||
avec et sans.
|
||||
6. **Faces de cadran : celles du modèle.** Les PNG `*_gauge_face_albedo*.png` sont retirés et
|
||||
`textures_are_loaded()` adapté à ce qui est réellement chargé.
|
||||
7. **`EmergencyDial` / « ÉJECTION · X » est conservé.** Le levier d'alarme sous capot rouge reste
|
||||
décoratif, capot fermé.
|
||||
8. **Dépôt : pas de Git LFS.** Git ordinaire, sans `.gitattributes`. Comme chaque version d'un `.glb`
|
||||
reste à jamais dans l'historique et que les textures bougent encore, **ne pas commiter les `.glb`
|
||||
sans l'accord explicite d'Olivier** : travailler avec les fichiers en place, non suivis, et commiter
|
||||
scène, code et tests à part. Fichiers sous `game/presentation/cockpit/assets/models/`
|
||||
(à côté des textures de cockpit existantes), noms en snake_case : `gauge_panel.glb`, `sonar.glb`,
|
||||
`switch_panel.glb`, `alert_box.glb`, `speaker.glb`. Les `.glb.import` ne sont pas versionnés, comme le
|
||||
reste de `.godot/` et des imports du dépôt.
|
||||
|
||||
## 11. Ce qui n'a jamais été vérifié
|
||||
|
||||
- Aucun des cinq `.glb` n'a été ouvert dans Godot. Import, orientation, échelle, matériaux, sens de
|
||||
rotation des aiguilles et des interrupteurs : tout est à constater.
|
||||
- L'aspect sous l'éclairage réel du cockpit : les modèles n'ont été vus que dans Blender.
|
||||
- La barrière qualité n'a pas été lancée pendant la rédaction de ce brief.
|
||||
- Les numéros de ligne cités datent du 2026-10-06 ; les relire avant de s'y fier.
|
||||
|
||||
## 12. Ordre de travail conseillé
|
||||
|
||||
1. Copier les `.glb` (hors commit), lancer `--import`, lire les journaux : zéro avertissement, sinon
|
||||
s'arrêter et le signaler — le correctif est côté modèles.
|
||||
2. Instancier les cinq modèles dans une scène d'essai, constater axes, échelle, hiérarchie et matériaux
|
||||
contre la section 3. Signaler tout écart avant d'aller plus loin.
|
||||
3. Placer dans `cockpit_interior.tscn` (section 4), mesurer les angles de vue.
|
||||
4. Brancher aiguilles, voyants, écran (section 5), avec leurs tests.
|
||||
5. Supprimer la géométrie procédurale remplacée, adapter le test d'intégration (section 7).
|
||||
6. Captures aux deux résolutions, à côté de l'image cible. Barrière verte.
|
||||
7. Compte rendu : ce qui est fait, ce qui diffère de ce brief, ce qui reste, ce qui n'a pas pu être vérifié.
|
||||
|
||||
En cas de défaut d'un modèle (trou visible, mauvais axe, texture fausse, nœud manquant) : ne pas le
|
||||
réparer dans Godot. Le décrire précisément, capture à l'appui ; il se corrige dans le script Blender qui
|
||||
a produit le fichier.
|
||||
@@ -1,37 +1,54 @@
|
||||
# Cockpit texture provenance
|
||||
# Cockpit asset provenance
|
||||
|
||||
These textures were generated specifically for Ransom's Wake on 3 October 2026
|
||||
## Textures
|
||||
|
||||
The texture below was 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`
|
||||
### `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`
|
||||
The source output was resized to 1024 × 1024 and stripped of metadata for
|
||||
runtime use. The original generated output remains in the local Codex image
|
||||
generation store and is not required by the game.
|
||||
|
||||
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.
|
||||
The generated gauge faces (`analog_gauge_face_albedo.png` and the four
|
||||
`*_gauge_face_albedo_v2.png`) were removed on 6 October 2026: the dials are now
|
||||
printed on the gauge panel model below.
|
||||
|
||||
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.
|
||||
## Models
|
||||
|
||||
## Dedicated instrument faces
|
||||
`models/gauge_panel.glb`, `models/sonar.glb`, `models/switch_panel.glb`,
|
||||
`models/alert_box.glb`, `models/speaker.glb`, `models/porthole.glb`,
|
||||
`models/hull_wall.glb`, `models/cage_lamp.glb`, `models/gas_bottle.glb`,
|
||||
`models/handrail_l.glb`, `models/handrail_r.glb` and `models/side_console.glb`
|
||||
were produced for Ransom's Wake
|
||||
by Blender scripts written for the project, from concept sheets generated for
|
||||
the project. No third-party mesh, texture or scan was used. Their textures
|
||||
(base colour, ORM, normal and, where relevant, emission) are embedded in the
|
||||
`.glb` files: 2048² for the gauge panel, sonar and switch panel, a 4096 × 1024
|
||||
strip for the four printed dials, 2048² for the porthole and hull wall, 1024²
|
||||
for the other props.
|
||||
|
||||
`oxygen_gauge_face_albedo_v2.png`, `battery_gauge_face_albedo_v2.png`,
|
||||
`hull_gauge_face_albedo_v2.png` and `depth_gauge_face_albedo_v2.png` were
|
||||
generated on 4 October 2026 with the same built-in tool. The approved cockpit
|
||||
target and then the oxygen face were used only as project-owned style
|
||||
references.
|
||||
Dial and label lettering is rendered into the textures with Arial and Impact,
|
||||
which are Windows system fonts. **Their licence must be checked before the game
|
||||
is distributed.**
|
||||
|
||||
Prompt summary: orthographic mechanical submarine gauges in aged ivory enamel,
|
||||
with exact French labels and units, dense numerical scales, restrained patina,
|
||||
and green, amber and red operating sectors. The generated needles, bezels and
|
||||
glass were explicitly excluded because those parts are modeled and animated in
|
||||
Godot. The source outputs were resized to 1024 × 1024 and given a circular alpha
|
||||
mask for flat, distortion-free mounting inside the physical bezels.
|
||||
The Blender sources are not in this repository. A model is changed by editing
|
||||
its generating script and re-exporting, never by editing the import in Godot.
|
||||
The cockpit scene references the `.glb` files directly, so a replaced file
|
||||
updates the cockpit without touching the scene.
|
||||
|
||||
With Godot's default import settings the embedded textures are extracted as
|
||||
`models/<model>_<texture>.png` next to each `.glb`. Those PNG files are import
|
||||
by-products, regenerated from the `.glb`, and are not sources.
|
||||
|
||||
As of 6 October 2026 the `.glb` files are deliberately **not committed**
|
||||
(their textures are still being balanced, and the repository uses plain Git
|
||||
without LFS). Until they are, `cockpit_interior.tscn` only loads on a machine
|
||||
that has them in `models/`.
|
||||
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,62 @@
|
||||
class_name CockpitAlertBox
|
||||
extends Node3D
|
||||
|
||||
## Drives the three lamps of the imported alert box model. Exactly one lamp is
|
||||
## lit for the alert level carried by the dive snapshot; the lamps are also
|
||||
## distinguished by position and by their engraved ALERTE / ATTENTION / DANGER
|
||||
## plates, so the reading does not depend on colour alone.
|
||||
|
||||
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
|
||||
const CockpitMeshFactory = preload("res://game/presentation/cockpit/cockpit_mesh_factory.gd")
|
||||
|
||||
const LAMP_NAMES: Array[String] = ["Lamp_Alerte", "Lamp_Attention", "Lamp_Danger"]
|
||||
const LIT_ENERGY := 4.0
|
||||
const UNLIT_ENERGY := 0.0
|
||||
|
||||
var _lamp_materials: Array[BaseMaterial3D] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for lamp_name: String in LAMP_NAMES:
|
||||
var lamp := find_child(lamp_name, true, false) as MeshInstance3D
|
||||
var material := CockpitMeshFactory.own_surface_material(lamp)
|
||||
if material == null:
|
||||
push_error("Alert box model has no usable %s lamp" % lamp_name)
|
||||
continue
|
||||
material.emission_energy_multiplier = UNLIT_ENERGY
|
||||
_lamp_materials.append(material)
|
||||
|
||||
|
||||
func present(alert_level: DiveAlertLevel.Value) -> void:
|
||||
var active_index := _lamp_index_for(alert_level)
|
||||
for lamp_index in _lamp_materials.size():
|
||||
_lamp_materials[lamp_index].emission_energy_multiplier = (
|
||||
LIT_ENERGY if lamp_index == active_index else UNLIT_ENERGY
|
||||
)
|
||||
|
||||
|
||||
func indicator_count() -> int:
|
||||
return _lamp_materials.size()
|
||||
|
||||
|
||||
func active_indicator_count() -> int:
|
||||
var count := 0
|
||||
for material: BaseMaterial3D in _lamp_materials:
|
||||
if material.emission_energy_multiplier > UNLIT_ENERGY:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func active_indicator_name() -> String:
|
||||
for lamp_index in _lamp_materials.size():
|
||||
if _lamp_materials[lamp_index].emission_energy_multiplier > UNLIT_ENERGY:
|
||||
return LAMP_NAMES[lamp_index]
|
||||
return ""
|
||||
|
||||
|
||||
func _lamp_index_for(alert_level: DiveAlertLevel.Value) -> int:
|
||||
if alert_level >= DiveAlertLevel.Value.CRITICAL:
|
||||
return 2
|
||||
if alert_level >= DiveAlertLevel.Value.ATTENTION:
|
||||
return 1
|
||||
return 0
|
||||
@@ -1,265 +1,101 @@
|
||||
class_name CockpitAnalogGauges
|
||||
extends Node3D
|
||||
|
||||
const CockpitMaterialPalette = preload("res://game/presentation/cockpit/cockpit_material_palette.gd")
|
||||
## Drives the imported gauge panel model: four needles and its six pilot lights.
|
||||
## The model scene is instanced by `cockpit_interior.tscn`; this script only
|
||||
## rotates needles and sets emission, it never rebuilds geometry.
|
||||
|
||||
const CockpitMeshFactory = preload("res://game/presentation/cockpit/cockpit_mesh_factory.gd")
|
||||
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
|
||||
|
||||
var _materials: CockpitMaterialPalette
|
||||
var _oxygen_needle: Node3D
|
||||
var _battery_needle: Node3D
|
||||
var _hull_needle: Node3D
|
||||
var _depth_needle: Node3D
|
||||
var _indicator_materials: Array[StandardMaterial3D] = []
|
||||
var _active_indicator_count := 0
|
||||
## Printed dial sweep: ratio 0 points lower-left, ratio 1 lower-right, clockwise.
|
||||
const NEEDLE_DEGREES_AT_ZERO := 120.0
|
||||
const NEEDLE_DEGREES_AT_FULL := -120.0
|
||||
## The snapshot carries no per-instrument lamp state, so the panel pilot lights
|
||||
## stay dark; the alert level is reported by `CockpitAlertBox`.
|
||||
const PILOT_LIGHT_REST_ENERGY := 0.0
|
||||
## Scales the dial backlight baked in the model (1.0 = as exported), so the
|
||||
## paper reads as lit ivory without outshining the cabin's warm lamps.
|
||||
const DIAL_BACKLIGHT_ENERGY := 0.7
|
||||
|
||||
const NEEDLE_NAMES: Array[String] = [
|
||||
"Needle_Oxygen",
|
||||
"Needle_Battery",
|
||||
"Needle_Hull",
|
||||
"Needle_Depth",
|
||||
]
|
||||
|
||||
var _needles: Dictionary[String, Node3D] = {}
|
||||
var _dials: MeshInstance3D
|
||||
var _pilot_light_materials: Array[BaseMaterial3D] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for needle_name: String in NEEDLE_NAMES:
|
||||
var needle := find_child(needle_name, true, false) as Node3D
|
||||
if needle == null:
|
||||
push_error("Gauge panel model has no %s node" % needle_name)
|
||||
continue
|
||||
_needles[needle_name] = needle
|
||||
_dials = find_child("GaugeDials", true, false) as MeshInstance3D
|
||||
var dial_material := CockpitMeshFactory.own_surface_material(_dials)
|
||||
if dial_material != null:
|
||||
dial_material.emission_energy_multiplier = DIAL_BACKLIGHT_ENERGY
|
||||
for light: Node in find_children("Light_*", "MeshInstance3D", true, false):
|
||||
var material := CockpitMeshFactory.own_surface_material(light as MeshInstance3D)
|
||||
if material != null:
|
||||
material.emission_energy_multiplier = PILOT_LIGHT_REST_ENERGY
|
||||
_pilot_light_materials.append(material)
|
||||
|
||||
func _init(materials: CockpitMaterialPalette) -> void:
|
||||
_materials = materials
|
||||
name = "AnalogGauges"
|
||||
_build_instrument_cluster()
|
||||
_build_indicator_bank()
|
||||
_oxygen_needle = _add_gauge(
|
||||
"OxygenGauge",
|
||||
Vector3(-0.52, 0.12, 0.145),
|
||||
0.94,
|
||||
_materials.oxygen_gauge_face
|
||||
)
|
||||
_battery_needle = _add_gauge(
|
||||
"BatteryGauge",
|
||||
Vector3(-0.22, 0.12, 0.145),
|
||||
0.94,
|
||||
_materials.battery_gauge_face
|
||||
)
|
||||
_hull_needle = _add_gauge(
|
||||
"HullGauge",
|
||||
Vector3(0.09, 0.12, 0.145),
|
||||
0.86,
|
||||
_materials.hull_gauge_face
|
||||
)
|
||||
_depth_needle = _add_gauge(
|
||||
"DepthGauge",
|
||||
Vector3(0.47, 0.12, 0.145),
|
||||
1.12,
|
||||
_materials.depth_gauge_face
|
||||
)
|
||||
|
||||
func present(
|
||||
oxygen_ratio: float,
|
||||
battery_ratio: float,
|
||||
hull_ratio: float,
|
||||
depth_ratio: float,
|
||||
alert_level: DiveAlertLevel.Value
|
||||
depth_ratio: float
|
||||
) -> void:
|
||||
_set_needle_ratio(_oxygen_needle, oxygen_ratio)
|
||||
_set_needle_ratio(_battery_needle, battery_ratio)
|
||||
_set_needle_ratio(_hull_needle, hull_ratio)
|
||||
_set_needle_ratio(_depth_needle, depth_ratio)
|
||||
_update_indicator_bank(alert_level)
|
||||
_set_needle_ratio("Needle_Oxygen", oxygen_ratio)
|
||||
_set_needle_ratio("Needle_Battery", battery_ratio)
|
||||
_set_needle_ratio("Needle_Hull", hull_ratio)
|
||||
_set_needle_ratio("Needle_Depth", depth_ratio)
|
||||
|
||||
|
||||
func gauge_count() -> int:
|
||||
return _needles.size()
|
||||
|
||||
|
||||
func pilot_light_count() -> int:
|
||||
return _pilot_light_materials.size()
|
||||
|
||||
|
||||
func lit_pilot_light_count() -> int:
|
||||
var count := 0
|
||||
for material: BaseMaterial3D in _pilot_light_materials:
|
||||
if material.emission_energy_multiplier > 0.0:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func needle_degrees(needle_name: String) -> float:
|
||||
var needle: Node3D = _needles.get(needle_name)
|
||||
return 0.0 if needle == null else needle.rotation_degrees.z
|
||||
|
||||
|
||||
func battery_needle_degrees() -> float:
|
||||
return 0.0 if _battery_needle == null else _battery_needle.rotation_degrees.z
|
||||
|
||||
func indicator_count() -> int:
|
||||
return _indicator_materials.size()
|
||||
|
||||
func gauge_count() -> int:
|
||||
return 4
|
||||
|
||||
func active_indicator_count() -> int:
|
||||
return _active_indicator_count
|
||||
return needle_degrees("Needle_Battery")
|
||||
|
||||
|
||||
func _build_instrument_cluster() -> void:
|
||||
CockpitMeshFactory.add_box(
|
||||
"InstrumentClusterHousing",
|
||||
self,
|
||||
Vector3(-0.04, 0.105, 0.095),
|
||||
Vector3(1.42, 0.31, 0.105),
|
||||
_materials.overhead_steel
|
||||
)
|
||||
CockpitMeshFactory.add_box(
|
||||
"InstrumentClusterFoot",
|
||||
self,
|
||||
Vector3(-0.04, -0.025, 0.075),
|
||||
Vector3(1.48, 0.040, 0.14),
|
||||
_materials.dark_steel
|
||||
)
|
||||
|
||||
func _build_indicator_bank() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "IndicatorBank"
|
||||
add_child(root)
|
||||
var colors: Array[Color] = [
|
||||
Color(0.08, 0.72, 0.38),
|
||||
Color(1.0, 0.46, 0.035),
|
||||
Color(0.92, 0.035, 0.018),
|
||||
]
|
||||
var captions: Array[String] = ["SYS", "ATTN", "ALRM"]
|
||||
for indicator_index in colors.size():
|
||||
var y := 0.165 - indicator_index * 0.050
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"%sBezel" % captions[indicator_index],
|
||||
root,
|
||||
Vector3(0.68, y, 0.156),
|
||||
0.016,
|
||||
0.012,
|
||||
_materials.brass,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
var material := _indicator_material(colors[indicator_index])
|
||||
_indicator_materials.append(material)
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"%sLens" % captions[indicator_index],
|
||||
root,
|
||||
Vector3(0.68, y, 0.165),
|
||||
0.009,
|
||||
0.008,
|
||||
material,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
var label := Label3D.new()
|
||||
label.name = "%sLabel" % captions[indicator_index]
|
||||
label.text = captions[indicator_index]
|
||||
label.font_size = 8
|
||||
label.pixel_size = 0.00042
|
||||
label.modulate = Color(0.70, 0.67, 0.56)
|
||||
label.position = Vector3(0.71, y, 0.168)
|
||||
root.add_child(label)
|
||||
|
||||
func _indicator_material(color: Color) -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
material.albedo_color = color * 0.12
|
||||
material.metallic = 0.05
|
||||
material.roughness = 0.16
|
||||
material.emission_enabled = true
|
||||
material.emission = color
|
||||
material.emission_energy_multiplier = 0.04
|
||||
return material
|
||||
|
||||
func _update_indicator_bank(alert_level: DiveAlertLevel.Value) -> void:
|
||||
var active_index := 0
|
||||
if alert_level >= DiveAlertLevel.Value.CRITICAL:
|
||||
active_index = 2
|
||||
elif alert_level >= DiveAlertLevel.Value.ATTENTION:
|
||||
active_index = 1
|
||||
for indicator_index in _indicator_materials.size():
|
||||
_indicator_materials[indicator_index].emission_energy_multiplier = (
|
||||
2.8 if indicator_index == active_index else 0.04
|
||||
)
|
||||
_active_indicator_count = 1
|
||||
func dial_texture_is_loaded() -> bool:
|
||||
if _dials == null or _dials.mesh == null or _dials.mesh.get_surface_count() == 0:
|
||||
return false
|
||||
var material := _dials.mesh.surface_get_material(0) as BaseMaterial3D
|
||||
return material != null and material.albedo_texture != null
|
||||
|
||||
|
||||
func _add_gauge(
|
||||
gauge_name: String,
|
||||
position: Vector3,
|
||||
gauge_scale: float,
|
||||
face_material: Material
|
||||
) -> Node3D:
|
||||
var gauge := Node3D.new()
|
||||
gauge.name = gauge_name
|
||||
gauge.position = position
|
||||
gauge.scale = Vector3(gauge_scale, gauge_scale, gauge_scale)
|
||||
add_child(gauge)
|
||||
CockpitMeshFactory.add_box(
|
||||
"MountingPlate",
|
||||
gauge,
|
||||
Vector3(0.0, 0.0, -0.025),
|
||||
Vector3(0.225, 0.225, 0.055),
|
||||
_materials.overhead_steel
|
||||
)
|
||||
for screw_position: Vector2 in [
|
||||
Vector2(-0.086, -0.086),
|
||||
Vector2(0.086, -0.086),
|
||||
Vector2(-0.086, 0.086),
|
||||
Vector2(0.086, 0.086),
|
||||
]:
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"MountingScrew",
|
||||
gauge,
|
||||
Vector3(screw_position.x, screw_position.y, 0.008),
|
||||
0.009,
|
||||
0.012,
|
||||
_materials.brass,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
_add_bezel(gauge, 0.108, 0.046, _materials.dark_steel)
|
||||
_add_bezel(gauge, 0.099, 0.041, _materials.brushed_steel)
|
||||
var face := MeshInstance3D.new()
|
||||
var printed_face := QuadMesh.new()
|
||||
printed_face.size = Vector2(0.178, 0.178)
|
||||
face.mesh = printed_face
|
||||
face.position.z = 0.031
|
||||
face.material_override = face_material
|
||||
gauge.add_child(face)
|
||||
var needle_root := Node3D.new()
|
||||
needle_root.name = "Needle"
|
||||
needle_root.position.z = 0.038
|
||||
gauge.add_child(needle_root)
|
||||
CockpitMeshFactory.add_box(
|
||||
"NeedleBlade",
|
||||
needle_root,
|
||||
Vector3(0.0, 0.030, 0.0),
|
||||
Vector3(0.006, 0.060, 0.006),
|
||||
_materials.dark_steel
|
||||
)
|
||||
CockpitMeshFactory.add_box(
|
||||
"NeedleTip",
|
||||
needle_root,
|
||||
Vector3(0.0, 0.061, 0.001),
|
||||
Vector3(0.004, 0.024, 0.006),
|
||||
_materials.needle_paint
|
||||
)
|
||||
CockpitMeshFactory.add_box(
|
||||
"NeedleCounterweight",
|
||||
needle_root,
|
||||
Vector3(0.0, -0.014, -0.001),
|
||||
Vector3(0.013, 0.030, 0.007),
|
||||
_materials.dark_steel
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"NeedleHub",
|
||||
needle_root,
|
||||
Vector3.ZERO,
|
||||
0.018,
|
||||
0.012,
|
||||
_materials.dark_steel,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
var glass := MeshInstance3D.new()
|
||||
glass.name = "GaugeGlass"
|
||||
var glass_mesh := CylinderMesh.new()
|
||||
glass_mesh.top_radius = 0.089
|
||||
glass_mesh.bottom_radius = 0.089
|
||||
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.050
|
||||
glass.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
gauge.add_child(glass)
|
||||
return needle_root
|
||||
|
||||
|
||||
func _add_bezel(
|
||||
gauge: Node3D,
|
||||
radius: float,
|
||||
height: float,
|
||||
material: Material
|
||||
) -> void:
|
||||
var bezel := MeshInstance3D.new()
|
||||
var bezel_mesh := CylinderMesh.new()
|
||||
bezel_mesh.top_radius = radius
|
||||
bezel_mesh.bottom_radius = radius
|
||||
bezel_mesh.height = height
|
||||
bezel_mesh.radial_segments = 48
|
||||
bezel.mesh = bezel_mesh
|
||||
bezel.material_override = material
|
||||
bezel.rotation_degrees.x = 90.0
|
||||
gauge.add_child(bezel)
|
||||
|
||||
|
||||
func _set_needle_ratio(needle: Node3D, ratio: float) -> void:
|
||||
func _set_needle_ratio(needle_name: String, ratio: float) -> void:
|
||||
var needle: Node3D = _needles.get(needle_name)
|
||||
if needle != null:
|
||||
needle.rotation_degrees.z = lerpf(125.0, -125.0, clampf(ratio, 0.0, 1.0))
|
||||
needle.rotation_degrees.z = lerpf(
|
||||
NEEDLE_DEGREES_AT_ZERO,
|
||||
NEEDLE_DEGREES_AT_FULL,
|
||||
clampf(ratio, 0.0, 1.0)
|
||||
)
|
||||
@@ -1,26 +1,12 @@
|
||||
class_name CockpitDashboardDetailing
|
||||
extends Node3D
|
||||
|
||||
const CockpitMaterialPalette = preload(
|
||||
"res://game/presentation/cockpit/cockpit_material_palette.gd"
|
||||
)
|
||||
const CockpitMeshFactory = preload(
|
||||
"res://game/presentation/cockpit/cockpit_mesh_factory.gd"
|
||||
)
|
||||
|
||||
var _materials: CockpitMaterialPalette
|
||||
var _control_count := 0
|
||||
## The dashboard keeps its hidden digital readouts (their labels still carry the
|
||||
## exact values); this hides the placeholder parts the gauge panel replaced.
|
||||
|
||||
|
||||
func _init(materials: CockpitMaterialPalette) -> void:
|
||||
_materials = materials
|
||||
func _init() -> void:
|
||||
name = "DashboardDetailing"
|
||||
_build_structural_frame()
|
||||
_build_switch_banks()
|
||||
|
||||
|
||||
func control_count() -> int:
|
||||
return _control_count
|
||||
|
||||
|
||||
func hide_obsolete_elements(cockpit: Node3D) -> void:
|
||||
@@ -42,79 +28,3 @@ func hide_obsolete_elements(cockpit: Node3D) -> void:
|
||||
var readout := cockpit.get_node_or_null("Dashboard/%s" % readout_name) as Node3D
|
||||
if readout != null:
|
||||
readout.visible = false
|
||||
|
||||
|
||||
func _build_structural_frame() -> void:
|
||||
CockpitMeshFactory.add_box(
|
||||
"TopRail",
|
||||
self,
|
||||
Vector3(0.0, 0.255, 0.115),
|
||||
Vector3(1.50, 0.055, 0.055),
|
||||
_materials.dark_steel
|
||||
)
|
||||
CockpitMeshFactory.add_box(
|
||||
"BottomRail",
|
||||
self,
|
||||
Vector3(0.0, -0.205, 0.115),
|
||||
Vector3(1.50, 0.050, 0.055),
|
||||
_materials.dark_steel
|
||||
)
|
||||
for x: float in [-0.755, 0.755]:
|
||||
CockpitMeshFactory.add_box(
|
||||
"SideRail",
|
||||
self,
|
||||
Vector3(x, 0.025, 0.115),
|
||||
Vector3(0.050, 0.46, 0.055),
|
||||
_materials.dark_steel
|
||||
)
|
||||
for screw_position: Vector2 in [
|
||||
Vector2(-0.72, -0.18),
|
||||
Vector2(-0.72, 0.23),
|
||||
Vector2(0.72, -0.18),
|
||||
Vector2(0.72, 0.23),
|
||||
Vector2(-0.24, -0.18),
|
||||
Vector2(0.24, -0.18),
|
||||
]:
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"PanelScrew",
|
||||
self,
|
||||
Vector3(screw_position.x, screw_position.y, 0.151),
|
||||
0.010,
|
||||
0.010,
|
||||
_materials.brass,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
|
||||
|
||||
func _build_switch_banks() -> void:
|
||||
_add_switch_bank(-0.59, "AUX")
|
||||
_add_switch_bank(0.59, "POMPES")
|
||||
|
||||
|
||||
func _add_switch_bank(center_x: float, caption: String) -> void:
|
||||
CockpitMeshFactory.add_box(
|
||||
"SwitchPlate",
|
||||
self,
|
||||
Vector3(center_x, -0.205, 0.145),
|
||||
Vector3(0.25, 0.060, 0.020),
|
||||
_materials.overhead_steel
|
||||
)
|
||||
for offset: float in [-0.075, -0.025, 0.025, 0.075]:
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"ToggleSwitch",
|
||||
self,
|
||||
Vector3(center_x + offset, -0.195, 0.171),
|
||||
0.006,
|
||||
0.030,
|
||||
_materials.dark_steel,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
_control_count += 1
|
||||
var label := Label3D.new()
|
||||
label.name = "%sLabel" % caption.to_pascal_case()
|
||||
label.text = caption
|
||||
label.font_size = 10
|
||||
label.pixel_size = 0.00055
|
||||
label.modulate = Color(0.72, 0.69, 0.56)
|
||||
label.position = Vector3(center_x, -0.235, 0.165)
|
||||
add_child(label)
|
||||
@@ -4,6 +4,7 @@ extends Node3D
|
||||
const DiveUpdate = preload("res://game/domain/dive/dive_update.gd")
|
||||
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
|
||||
const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd")
|
||||
const CockpitAlertBox = preload("res://game/presentation/cockpit/cockpit_alert_box.gd")
|
||||
const CockpitAnalogGauges = preload("res://game/presentation/cockpit/cockpit_analog_gauges.gd")
|
||||
const CockpitDashboardDetailing = preload(
|
||||
"res://game/presentation/cockpit/cockpit_dashboard_detailing.gd"
|
||||
@@ -19,13 +20,17 @@ const CockpitSonarConsole = preload(
|
||||
const CockpitPracticalLighting = preload(
|
||||
"res://game/presentation/cockpit/cockpit_practical_lighting.gd"
|
||||
)
|
||||
const CockpitSwitchPanel = preload(
|
||||
"res://game/presentation/cockpit/cockpit_switch_panel.gd"
|
||||
)
|
||||
|
||||
var _materials: CockpitMaterialPalette
|
||||
var _painted_steel: Material
|
||||
var _dark_steel: Material
|
||||
var _brass: Material
|
||||
var _copper: Material
|
||||
var _analog_gauges: CockpitAnalogGauges
|
||||
var _alert_box: CockpitAlertBox
|
||||
var _switch_panel: CockpitSwitchPanel
|
||||
var _gas_bottle: Node3D
|
||||
var _dashboard_detailing: CockpitDashboardDetailing
|
||||
var _side_equipment: CockpitSideEquipment
|
||||
var _sonar_console: CockpitSonarConsole
|
||||
@@ -35,9 +40,6 @@ var _practical_lighting: CockpitPracticalLighting
|
||||
func _ready() -> void:
|
||||
_create_materials()
|
||||
_apply_materials_to_existing_cockpit()
|
||||
_build_pressure_hull_ribs()
|
||||
_build_life_support_pipes()
|
||||
_build_panel_hardware()
|
||||
_build_dashboard_instrumentation()
|
||||
_build_equipment_assemblies()
|
||||
|
||||
@@ -46,32 +48,24 @@ func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void:
|
||||
if update == null or update.snapshot == null or telemetry == null:
|
||||
return
|
||||
var snapshot = update.snapshot
|
||||
_analog_gauges.present(
|
||||
snapshot.oxygen.ratio,
|
||||
snapshot.battery.ratio,
|
||||
snapshot.hull.ratio,
|
||||
clampf(
|
||||
snapshot.depth.current_m
|
||||
/ maxf(snapshot.depth.certified_m, 0.001),
|
||||
0.0,
|
||||
1.0
|
||||
),
|
||||
snapshot.alerts.maximum
|
||||
)
|
||||
var alert_level: DiveAlertLevel.Value = snapshot.alerts.maximum
|
||||
if _analog_gauges != null:
|
||||
_analog_gauges.present(
|
||||
snapshot.oxygen.ratio,
|
||||
snapshot.battery.ratio,
|
||||
snapshot.hull.ratio,
|
||||
clampf(
|
||||
snapshot.depth.current_m
|
||||
/ maxf(snapshot.depth.certified_m, 0.001),
|
||||
0.0,
|
||||
1.0
|
||||
)
|
||||
)
|
||||
if _alert_box != null:
|
||||
_alert_box.present(alert_level)
|
||||
_practical_lighting.present(alert_level)
|
||||
|
||||
|
||||
func pressure_hull_rib_count() -> int:
|
||||
var ribs := get_node_or_null("PressureHullRibs")
|
||||
return 0 if ribs == null else ribs.get_child_count()
|
||||
|
||||
|
||||
func pipe_segment_count() -> int:
|
||||
var pipes := get_node_or_null("LifeSupportPipes")
|
||||
return 0 if pipes == null else pipes.get_child_count()
|
||||
|
||||
|
||||
func battery_needle_degrees() -> float:
|
||||
return 0.0 if _analog_gauges == null else _analog_gauges.battery_needle_degrees()
|
||||
|
||||
@@ -80,25 +74,30 @@ func textures_are_loaded() -> bool:
|
||||
return (
|
||||
_materials != null
|
||||
and _materials.overhead_steel.albedo_texture != null
|
||||
and _materials.oxygen_gauge_face.albedo_texture != null
|
||||
and _materials.battery_gauge_face.albedo_texture != null
|
||||
and _materials.hull_gauge_face.albedo_texture != null
|
||||
and _materials.depth_gauge_face.albedo_texture != null
|
||||
and _analog_gauges != null
|
||||
and _analog_gauges.dial_texture_is_loaded()
|
||||
)
|
||||
|
||||
|
||||
func dashboard_control_count() -> int:
|
||||
return 0 if _dashboard_detailing == null else _dashboard_detailing.control_count()
|
||||
return 0 if _switch_panel == null else _switch_panel.control_count()
|
||||
|
||||
|
||||
func side_equipment_count() -> int:
|
||||
if _side_equipment == null:
|
||||
return 0
|
||||
return _side_equipment.tank_count() + _side_equipment.junction_box_count()
|
||||
return _side_equipment.junction_box_count() + (0 if _gas_bottle == null else 1)
|
||||
|
||||
|
||||
func side_control_count() -> int:
|
||||
return 0 if _side_equipment == null else _side_equipment.control_count()
|
||||
if _side_equipment == null:
|
||||
return 0
|
||||
var bottle_valve_count := (
|
||||
0
|
||||
if _gas_bottle == null
|
||||
else _gas_bottle.find_children("*ValveWheel", "Node3D", true, false).size()
|
||||
)
|
||||
return _side_equipment.control_count() + bottle_valve_count
|
||||
|
||||
|
||||
func sonar_control_count() -> int:
|
||||
@@ -117,8 +116,6 @@ func _create_materials() -> void:
|
||||
_materials = CockpitMaterialPalette.new()
|
||||
_painted_steel = _materials.painted_steel
|
||||
_dark_steel = _materials.dark_steel
|
||||
_brass = _materials.brass
|
||||
_copper = _materials.copper
|
||||
|
||||
|
||||
func _apply_materials_to_existing_cockpit() -> void:
|
||||
@@ -127,39 +124,12 @@ func _apply_materials_to_existing_cockpit() -> void:
|
||||
[
|
||||
"CabinShell/LeftWall",
|
||||
"CabinShell/RightWall",
|
||||
"LeftConsole",
|
||||
"RightConsole",
|
||||
]
|
||||
)
|
||||
_apply_material_to_paths(
|
||||
_dark_steel,
|
||||
[
|
||||
"CabinShell/RearBulkhead",
|
||||
"PortholeAssembly/LeftBulkhead",
|
||||
"PortholeAssembly/RightBulkhead",
|
||||
"PortholeAssembly/BottomBulkhead",
|
||||
"ThrottleLever",
|
||||
"VerticalLever",
|
||||
]
|
||||
)
|
||||
_apply_material_to_paths(_dark_steel, ["CabinShell/RearBulkhead"])
|
||||
_apply_material_to_paths(
|
||||
_materials.overhead_steel,
|
||||
[
|
||||
"CabinShell/Ceiling",
|
||||
"PortholeAssembly/TopBulkhead",
|
||||
"Dashboard/MainPanel",
|
||||
]
|
||||
)
|
||||
_apply_material_to_paths(
|
||||
_brass,
|
||||
[
|
||||
"PortholeAssembly/WindowFrame",
|
||||
"Dashboard/ResourcesFrame",
|
||||
"Dashboard/NavigationFrame",
|
||||
"Dashboard/IntegrityFrame",
|
||||
"ThrottleKnob",
|
||||
"VerticalKnob",
|
||||
]
|
||||
["CabinShell/Ceiling"]
|
||||
)
|
||||
func _apply_material_to_paths(material: Material, paths: Array[String]) -> void:
|
||||
var cockpit := get_parent()
|
||||
@@ -169,128 +139,26 @@ func _apply_material_to_paths(material: Material, paths: Array[String]) -> void:
|
||||
mesh.material_override = material
|
||||
|
||||
|
||||
func _build_pressure_hull_ribs() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "PressureHullRibs"
|
||||
add_child(root)
|
||||
for rib_index in 2:
|
||||
var rib := MeshInstance3D.new()
|
||||
rib.name = "CircularRib%d" % (rib_index + 1)
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = 1.00
|
||||
torus.outer_radius = 1.075
|
||||
torus.rings = 48
|
||||
torus.ring_segments = 10
|
||||
rib.mesh = torus
|
||||
rib.material_override = _dark_steel
|
||||
rib.position = Vector3(0.0, 0.22, -0.90 + rib_index * 1.35)
|
||||
rib.rotation_degrees.x = 90.0
|
||||
root.add_child(rib)
|
||||
|
||||
for side: float in [-1.0, 1.0]:
|
||||
CockpitMeshFactory.add_box(
|
||||
"AngledShoulder",
|
||||
root,
|
||||
Vector3(side * 0.88, 0.84, -0.36),
|
||||
Vector3(0.11, 0.62, 1.52),
|
||||
_painted_steel,
|
||||
Vector3(0.0, 0.0, side * -18.0)
|
||||
)
|
||||
CockpitMeshFactory.add_box(
|
||||
"LowerStringer",
|
||||
root,
|
||||
Vector3(side * 0.91, -0.38, -0.18),
|
||||
Vector3(0.10, 0.12, 1.70),
|
||||
_dark_steel
|
||||
)
|
||||
|
||||
|
||||
func _build_life_support_pipes() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "LifeSupportPipes"
|
||||
add_child(root)
|
||||
for side: float in [-1.0, 1.0]:
|
||||
var x: float = side * 0.83
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"VerticalPipe",
|
||||
root,
|
||||
Vector3(x, 0.33, -1.12),
|
||||
0.027,
|
||||
1.18,
|
||||
_copper
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"ReturnPipe",
|
||||
root,
|
||||
Vector3(side * 0.73, 0.47, -1.09),
|
||||
0.019,
|
||||
0.92,
|
||||
_brass
|
||||
)
|
||||
for clamp_y: float in [-0.04, 0.35, 0.72]:
|
||||
var clamp := MeshInstance3D.new()
|
||||
clamp.name = "PipeClamp"
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = 0.027
|
||||
torus.outer_radius = 0.041
|
||||
torus.rings = 16
|
||||
torus.ring_segments = 6
|
||||
clamp.mesh = torus
|
||||
clamp.material_override = _dark_steel
|
||||
clamp.position = Vector3(x, clamp_y, -1.12)
|
||||
root.add_child(clamp)
|
||||
CockpitMeshFactory.add_sphere(
|
||||
"PipeJunction",
|
||||
root,
|
||||
Vector3(x, 0.91, -1.12),
|
||||
0.052,
|
||||
_copper
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder_between(
|
||||
"UpperBranchPipe",
|
||||
root,
|
||||
Vector3(x, 0.84, -1.12),
|
||||
Vector3(side * 0.72, 0.77, -1.10),
|
||||
0.019,
|
||||
_brass
|
||||
)
|
||||
|
||||
|
||||
func _build_panel_hardware() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "PanelHardware"
|
||||
add_child(root)
|
||||
for x: float in [-0.73, -0.49, -0.24, 0.24, 0.49, 0.73]:
|
||||
for y: float in [-0.18, 0.22]:
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"PanelFastener",
|
||||
root,
|
||||
Vector3(x, y, -1.105),
|
||||
0.014,
|
||||
0.018,
|
||||
_dark_steel,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
|
||||
|
||||
|
||||
func _build_dashboard_instrumentation() -> void:
|
||||
var dashboard := get_parent().get_node_or_null("Dashboard") as Node3D
|
||||
if dashboard == null:
|
||||
return
|
||||
_dashboard_detailing = CockpitDashboardDetailing.new(_materials)
|
||||
_dashboard_detailing = CockpitDashboardDetailing.new()
|
||||
dashboard.add_child(_dashboard_detailing)
|
||||
_dashboard_detailing.hide_obsolete_elements(get_parent())
|
||||
_analog_gauges = CockpitAnalogGauges.new(_materials)
|
||||
dashboard.add_child(_analog_gauges)
|
||||
_analog_gauges = dashboard.get_node_or_null("AnalogGauges") as CockpitAnalogGauges
|
||||
|
||||
|
||||
func _build_equipment_assemblies() -> void:
|
||||
_side_equipment = CockpitSideEquipment.new(_materials)
|
||||
add_child(_side_equipment)
|
||||
var sonar_monitor := get_parent().get_node_or_null("SonarMonitor") as Node3D
|
||||
if sonar_monitor != null:
|
||||
_sonar_console = CockpitSonarConsole.new(_materials)
|
||||
sonar_monitor.add_child(_sonar_console)
|
||||
var cockpit := get_parent()
|
||||
_sonar_console = cockpit.get_node_or_null("SonarMonitor") as CockpitSonarConsole
|
||||
_alert_box = cockpit.get_node_or_null("AlertBox") as CockpitAlertBox
|
||||
_switch_panel = cockpit.get_node_or_null("SwitchPanel") as CockpitSwitchPanel
|
||||
_gas_bottle = cockpit.get_node_or_null("GasBottle") as Node3D
|
||||
_practical_lighting = CockpitPracticalLighting.new(_materials)
|
||||
add_child(_practical_lighting)
|
||||
# The alert box hangs between the left lamp and the gas bottle: casting, it
|
||||
# puts the bottle in full shadow.
|
||||
CockpitMeshFactory.disable_shadow_casting(_alert_box)
|
||||
@@ -12,6 +12,9 @@ const CockpitDetailing = preload(
|
||||
const COLOR_NORMAL := Color(0.45, 0.89, 0.90, 1.0)
|
||||
const COLOR_ATTENTION := Color(1.0, 0.75, 0.20, 1.0)
|
||||
const COLOR_CRITICAL := Color(1.0, 0.42, 0.31, 1.0)
|
||||
## 1.0 shows the sonar picture as rendered; slightly above lets its brightest
|
||||
## lines bloom without clipping the surface colours.
|
||||
const SONAR_SCREEN_ENERGY := 1.25
|
||||
|
||||
@onready var _resources_label: Label3D = %ResourcesLabel
|
||||
@onready var _navigation_label: Label3D = %NavigationLabel
|
||||
@@ -84,12 +87,18 @@ func return_text() -> String:
|
||||
|
||||
func attach_sonar_texture(texture: Texture2D) -> void:
|
||||
_sonar_texture = texture
|
||||
# The glass shows the picture by emission only: black albedo, no specular,
|
||||
# so cabin lights add nothing to it, and the energy can lift the brightest
|
||||
# lines just above 1.0 for the environment glow to pick up.
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.albedo_texture = texture
|
||||
material.albedo_color = Color.BLACK
|
||||
material.metallic_specular = 0.0
|
||||
material.roughness = 1.0
|
||||
material.emission_enabled = true
|
||||
material.emission = Color.WHITE
|
||||
material.emission_operator = BaseMaterial3D.EMISSION_OP_MULTIPLY
|
||||
material.emission_texture = texture
|
||||
material.emission_energy_multiplier = 1.7
|
||||
material.emission_energy_multiplier = SONAR_SCREEN_ENERGY
|
||||
_sonar_screen.material_override = material
|
||||
_sonar_placeholder.visible = false
|
||||
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
[gd_scene load_steps=21 format=3]
|
||||
[gd_scene load_steps=31 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_instrument_panel.gd" id="1_instruments"]
|
||||
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_detailing.gd" id="2_detailing"]
|
||||
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_pressure_bulkhead.gd" id="3_bulkhead"]
|
||||
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_analog_gauges.gd" id="4_gauges"]
|
||||
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_sonar_console.gd" id="5_sonar_console"]
|
||||
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_alert_box.gd" id="6_alert_box"]
|
||||
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_switch_panel.gd" id="7_switch_panel"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/gauge_panel.glb" id="8_gauge_panel_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/sonar.glb" id="9_sonar_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/switch_panel.glb" id="10_switch_panel_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/alert_box.glb" id="11_alert_box_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/speaker.glb" id="12_speaker_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/hull_wall.glb" id="13_hull_wall_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/porthole.glb" id="14_porthole_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/gas_bottle.glb" id="15_gas_bottle_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/handrail_l.glb" id="16_handrail_l_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/handrail_r.glb" id="17_handrail_r_model"]
|
||||
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/assets/models/side_console.glb" id="18_side_console_model"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="SteelMaterial"]
|
||||
albedo_color = Color(0.055, 0.07, 0.07, 1)
|
||||
@@ -23,13 +38,6 @@ roughness = 0.43
|
||||
albedo_color = Color(0.018, 0.024, 0.025, 1)
|
||||
roughness = 0.88
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="GlassMaterial"]
|
||||
transparency = 1
|
||||
albedo_color = Color(0.08, 0.29, 0.32, 0.12)
|
||||
metallic = 0.05
|
||||
roughness = 0.16
|
||||
cull_mode = 2
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="ScreenMaterial"]
|
||||
albedo_color = Color(0.006, 0.025, 0.028, 1)
|
||||
metallic = 0.15
|
||||
@@ -62,33 +70,9 @@ emission_energy_multiplier = 2.8
|
||||
[sub_resource type="BoxMesh" id="UnitBoxMesh"]
|
||||
size = Vector3(1, 1, 1)
|
||||
|
||||
[sub_resource type="TorusMesh" id="PortholeTorusMesh"]
|
||||
inner_radius = 0.56
|
||||
outer_radius = 0.71
|
||||
rings = 48
|
||||
ring_segments = 16
|
||||
|
||||
[sub_resource type="CylinderMesh" id="GlassCylinderMesh"]
|
||||
top_radius = 0.56
|
||||
bottom_radius = 0.56
|
||||
height = 0.012
|
||||
radial_segments = 64
|
||||
|
||||
[sub_resource type="CylinderMesh" id="BoltMesh"]
|
||||
top_radius = 0.026
|
||||
bottom_radius = 0.026
|
||||
height = 0.045
|
||||
radial_segments = 12
|
||||
|
||||
[sub_resource type="QuadMesh" id="ScreenQuadMesh"]
|
||||
size = Vector2(1, 1)
|
||||
|
||||
[sub_resource type="CylinderMesh" id="LeverMesh"]
|
||||
top_radius = 0.018
|
||||
bottom_radius = 0.023
|
||||
height = 0.24
|
||||
radial_segments = 12
|
||||
|
||||
[sub_resource type="SphereMesh" id="KnobMesh"]
|
||||
radius = 0.046
|
||||
height = 0.092
|
||||
@@ -111,20 +95,20 @@ script = ExtResource("2_detailing")
|
||||
[node name="CabinShell" type="Node3D" parent="."]
|
||||
|
||||
[node name="LeftWall" type="MeshInstance3D" parent="CabinShell"]
|
||||
position = Vector3(-1.03, 0.24, -0.15)
|
||||
scale = Vector3(0.16, 1.82, 2.7)
|
||||
position = Vector3(-1.25, 0.47, 0.53)
|
||||
scale = Vector3(0.16, 2.62, 1.1)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("PaintedMaterial")
|
||||
|
||||
[node name="RightWall" type="MeshInstance3D" parent="CabinShell"]
|
||||
position = Vector3(1.03, 0.24, -0.15)
|
||||
scale = Vector3(0.16, 1.82, 2.7)
|
||||
position = Vector3(1.25, 0.47, 0.53)
|
||||
scale = Vector3(0.16, 2.62, 1.1)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("PaintedMaterial")
|
||||
|
||||
[node name="Ceiling" type="MeshInstance3D" parent="CabinShell"]
|
||||
position = Vector3(0, 1.22, -0.2)
|
||||
scale = Vector3(2.15, 0.16, 2.6)
|
||||
position = Vector3(0, 1.72, 0.53)
|
||||
scale = Vector3(2.66, 0.16, 1.1)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
@@ -135,29 +119,11 @@ mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("RubberMaterial")
|
||||
|
||||
[node name="RearBulkhead" type="MeshInstance3D" parent="CabinShell"]
|
||||
position = Vector3(0, 0.22, 1.08)
|
||||
scale = Vector3(2.08, 1.75, 0.16)
|
||||
position = Vector3(0, 0.47, 1.08)
|
||||
scale = Vector3(2.66, 2.62, 0.16)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="LeftRib" type="MeshInstance3D" parent="CabinShell"]
|
||||
position = Vector3(-0.88, 0.26, -0.45)
|
||||
scale = Vector3(0.11, 1.75, 0.18)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("BrassMaterial")
|
||||
|
||||
[node name="RightRib" type="MeshInstance3D" parent="CabinShell"]
|
||||
position = Vector3(0.88, 0.26, -0.45)
|
||||
scale = Vector3(0.11, 1.75, 0.18)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("BrassMaterial")
|
||||
|
||||
[node name="CeilingRib" type="MeshInstance3D" parent="CabinShell"]
|
||||
position = Vector3(0, 1.05, -0.45)
|
||||
scale = Vector3(1.82, 0.1, 0.18)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("BrassMaterial")
|
||||
|
||||
[node name="PortholeAssembly" type="Node3D" parent="."]
|
||||
position = Vector3(0, 0.47, -1.32)
|
||||
|
||||
@@ -165,107 +131,15 @@ position = Vector3(0, 0.47, -1.32)
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("3_bulkhead")
|
||||
|
||||
[node name="LeftBulkhead" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(-0.94, 0, 0)
|
||||
scale = Vector3(0.6, 1.82, 0.2)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
[node name="HullWall" parent="PortholeAssembly/CurvedPressureBulkhead" instance=ExtResource("13_hull_wall_model")]
|
||||
|
||||
[node name="RightBulkhead" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(0.94, 0, 0)
|
||||
scale = Vector3(0.6, 1.82, 0.2)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="TopBulkhead" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(0, 0.91, 0)
|
||||
scale = Vector3(1.55, 0.52, 0.2)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="BottomBulkhead" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(0, -0.91, 0)
|
||||
scale = Vector3(1.55, 0.52, 0.2)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="WindowFrame" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
unique_name_in_owner = true
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("PortholeTorusMesh")
|
||||
material_override = SubResource("BrassMaterial")
|
||||
|
||||
[node name="RubberSeal" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
scale = Vector3(0.91, 0.91, 0.91)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("PortholeTorusMesh")
|
||||
material_override = SubResource("RubberMaterial")
|
||||
|
||||
[node name="WindowGlass" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(0, 0, -0.01)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("GlassCylinderMesh")
|
||||
material_override = SubResource("GlassMaterial")
|
||||
|
||||
[node name="BoltTop" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(0, 0.665, 0.1)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("BoltMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="BoltBottom" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(0, -0.665, 0.1)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("BoltMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="BoltLeft" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(-0.665, 0, 0.1)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("BoltMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="BoltRight" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(0.665, 0, 0.1)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("BoltMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="BoltUpperLeft" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(-0.47, 0.47, 0.1)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("BoltMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="BoltUpperRight" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(0.47, 0.47, 0.1)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("BoltMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="BoltLowerLeft" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(-0.47, -0.47, 0.1)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("BoltMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="BoltLowerRight" type="MeshInstance3D" parent="PortholeAssembly"]
|
||||
position = Vector3(0.47, -0.47, 0.1)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("BoltMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
[node name="Porthole" parent="PortholeAssembly/CurvedPressureBulkhead" instance=ExtResource("14_porthole_model")]
|
||||
|
||||
[node name="Dashboard" type="Node3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 0.05, -1.22)
|
||||
rotation_degrees = Vector3(-12, 0, 0)
|
||||
|
||||
[node name="MainPanel" type="MeshInstance3D" parent="Dashboard"]
|
||||
scale = Vector3(1.56, 0.44, 0.14)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("PaintedMaterial")
|
||||
|
||||
[node name="ResourcesFrame" type="MeshInstance3D" parent="Dashboard"]
|
||||
position = Vector3(-0.49, -0.115, 0.14)
|
||||
scale = Vector3(0.34, 0.065, 0.025)
|
||||
@@ -331,7 +205,7 @@ modulate = Color(0.76, 0.74, 0.6, 1)
|
||||
|
||||
[node name="ReturnLabel" type="Label3D" parent="Dashboard"]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0.61, 0.245, 0.17)
|
||||
position = Vector3(0.22, 0.266, 0.158)
|
||||
pixel_size = 0.00062
|
||||
font_size = 12
|
||||
outline_size = 2
|
||||
@@ -340,89 +214,86 @@ modulate = Color(1, 0.75, 0.2, 1)
|
||||
|
||||
[node name="StatusLabel" type="Label3D" parent="Dashboard"]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(-0.61, 0.245, 0.17)
|
||||
position = Vector3(-0.36, 0.266, 0.158)
|
||||
pixel_size = 0.00062
|
||||
font_size = 12
|
||||
outline_size = 2
|
||||
text = "SYSTÈMES · NORMAL"
|
||||
modulate = Color(0.45, 0.89, 0.9, 1)
|
||||
|
||||
[node name="LeftConsole" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.82, -0.23, -1.19)
|
||||
rotation_degrees = Vector3(-8, 18, 4)
|
||||
scale = Vector3(0.32, 0.16, 0.56)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("PaintedMaterial")
|
||||
[node name="AnalogGauges" type="Node3D" parent="Dashboard"]
|
||||
position = Vector3(0, -0.06, 0.15)
|
||||
script = ExtResource("4_gauges")
|
||||
|
||||
[node name="RightConsole" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.82, -0.23, -1.19)
|
||||
rotation_degrees = Vector3(-8, -18, -4)
|
||||
scale = Vector3(0.32, 0.16, 0.56)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("PaintedMaterial")
|
||||
|
||||
[node name="ThrottleLever" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.79, -0.06, -1.13)
|
||||
rotation_degrees = Vector3(18, 0, -10)
|
||||
scale = Vector3(0.72, 0.72, 0.72)
|
||||
mesh = SubResource("LeverMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="ThrottleKnob" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.81, 0.025, -1.1)
|
||||
scale = Vector3(0.72, 0.72, 0.72)
|
||||
mesh = SubResource("KnobMesh")
|
||||
material_override = SubResource("BrassMaterial")
|
||||
|
||||
[node name="VerticalLever" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.79, -0.06, -1.13)
|
||||
rotation_degrees = Vector3(18, 0, 10)
|
||||
scale = Vector3(0.72, 0.72, 0.72)
|
||||
mesh = SubResource("LeverMesh")
|
||||
material_override = SubResource("SteelMaterial")
|
||||
|
||||
[node name="VerticalKnob" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.81, 0.025, -1.1)
|
||||
scale = Vector3(0.72, 0.72, 0.72)
|
||||
mesh = SubResource("KnobMesh")
|
||||
material_override = SubResource("BrassMaterial")
|
||||
[node name="GaugePanelModel" parent="Dashboard/AnalogGauges" instance=ExtResource("8_gauge_panel_model")]
|
||||
|
||||
[node name="SonarMonitor" type="Node3D" parent="."]
|
||||
position = Vector3(0.54, 0.59, -1.04)
|
||||
position = Vector3(0.54, 0.48, -1.04)
|
||||
rotation_degrees = Vector3(-5, -42, -2)
|
||||
scale = Vector3(0.6, 0.6, 0.6)
|
||||
script = ExtResource("5_sonar_console")
|
||||
|
||||
[node name="Housing" type="MeshInstance3D" parent="SonarMonitor"]
|
||||
scale = Vector3(0.6, 0.37, 0.1)
|
||||
mesh = SubResource("UnitBoxMesh")
|
||||
material_override = SubResource("PaintedMaterial")
|
||||
|
||||
[node name="SonarScreen" type="MeshInstance3D" parent="SonarMonitor"]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(-0.025, 0.012, 0.06)
|
||||
scale = Vector3(0.36, 0.235, 1)
|
||||
mesh = SubResource("ScreenQuadMesh")
|
||||
material_override = SubResource("ScreenMaterial")
|
||||
[node name="SonarModel" parent="SonarMonitor" instance=ExtResource("9_sonar_model")]
|
||||
scale = Vector3(0.31, 0.31, 0.31)
|
||||
|
||||
[node name="SonarPlaceholder" type="Label3D" parent="SonarMonitor"]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(-0.025, 0.012, 0.068)
|
||||
pixel_size = 0.0009
|
||||
position = Vector3(-0.0164, 0.002, 0.026)
|
||||
pixel_size = 0.00054
|
||||
font_size = 16
|
||||
outline_size = 2
|
||||
text = "SONAR\nMODULE EN ATTENTE"
|
||||
text = "SONAR
|
||||
MODULE EN ATTENTE"
|
||||
modulate = Color(0.34, 0.6, 0.6, 1)
|
||||
|
||||
[node name="AlertBox" type="Node3D" parent="."]
|
||||
position = Vector3(-0.86, 0.555, -1.214)
|
||||
rotation_degrees = Vector3(0, 15, 0)
|
||||
script = ExtResource("6_alert_box")
|
||||
|
||||
[node name="AlertBoxModel" parent="AlertBox" instance=ExtResource("11_alert_box_model")]
|
||||
scale = Vector3(0.5, 0.5, 0.5)
|
||||
|
||||
[node name="SwitchPanel" type="Node3D" parent="."]
|
||||
position = Vector3(-0.74, -0.125, -0.97)
|
||||
rotation_degrees = Vector3(0, 30, 0)
|
||||
script = ExtResource("7_switch_panel")
|
||||
|
||||
[node name="SwitchPanelModel" parent="SwitchPanel" instance=ExtResource("10_switch_panel_model")]
|
||||
scale = Vector3(0.37, 0.37, 0.37)
|
||||
|
||||
[node name="Speaker" parent="." instance=ExtResource("12_speaker_model")]
|
||||
position = Vector3(0.83, 0.84, -1.22)
|
||||
rotation_degrees = Vector3(0, -15, 0)
|
||||
scale = Vector3(0.56, 0.56, 0.56)
|
||||
|
||||
[node name="GasBottle" parent="." instance=ExtResource("15_gas_bottle_model")]
|
||||
position = Vector3(-1.02, 0.36, -1.225)
|
||||
rotation_degrees = Vector3(0, 53.3, 0)
|
||||
scale = Vector3(0.65, 0.65, 0.65)
|
||||
|
||||
[node name="HandrailLeft" parent="." instance=ExtResource("16_handrail_l_model")]
|
||||
position = Vector3(-0.6, 0.24, -1.1)
|
||||
|
||||
[node name="HandrailRight" parent="." instance=ExtResource("17_handrail_r_model")]
|
||||
position = Vector3(0.6, 0.24, -1.1)
|
||||
|
||||
[node name="SideConsoleLeft" parent="." instance=ExtResource("18_side_console_model")]
|
||||
position = Vector3(-0.632, -0.61, -1.215)
|
||||
|
||||
[node name="SideConsoleRight" parent="." instance=ExtResource("18_side_console_model")]
|
||||
position = Vector3(0.632, -0.61, -1.215)
|
||||
scale = Vector3(-1, 1, 1)
|
||||
|
||||
[node name="EmergencyDial" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.7, 0.32, -1.15)
|
||||
scale = Vector3(0.18, 0.18, 0.18)
|
||||
position = Vector3(-0.845, 0.47, -1.283)
|
||||
scale = Vector3(0.3, 0.3, 0.3)
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("DialMesh")
|
||||
material_override = SubResource("RedMaterial")
|
||||
|
||||
[node name="EmergencyLabel" type="Label3D" parent="."]
|
||||
position = Vector3(-0.7, 0.275, -1.125)
|
||||
pixel_size = 0.00072
|
||||
position = Vector3(-0.845, 0.418, -1.28)
|
||||
pixel_size = 0.0015
|
||||
font_size = 12
|
||||
outline_size = 2
|
||||
text = "ÉJECTION · X"
|
||||
@@ -449,15 +320,15 @@ material_override = SubResource("RedMaterial")
|
||||
[node name="CabinLight" type="OmniLight3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 0.92, -0.28)
|
||||
light_color = Color(0.36, 0.68, 0.69, 1)
|
||||
light_energy = 1.25
|
||||
light_color = Color(1, 0.7, 0.46, 1)
|
||||
light_energy = 0.04
|
||||
shadow_enabled = true
|
||||
omni_range = 3.3
|
||||
|
||||
[node name="PanelLight" type="OmniLight3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 0.18, -0.63)
|
||||
light_color = Color(0.23, 0.72, 0.76, 1)
|
||||
light_energy = 0.58
|
||||
light_color = Color(1, 0.72, 0.5, 1)
|
||||
light_energy = 0.4
|
||||
shadow_enabled = false
|
||||
omni_range = 1.35
|
||||
omni_range = 0.8
|
||||
@@ -4,18 +4,6 @@ extends RefCounted
|
||||
const NAVAL_STEEL_TEXTURE_PATH := (
|
||||
"res://game/presentation/cockpit/assets/naval_painted_steel_albedo.png"
|
||||
)
|
||||
const OXYGEN_GAUGE_FACE_TEXTURE_PATH := (
|
||||
"res://game/presentation/cockpit/assets/oxygen_gauge_face_albedo_v2.png"
|
||||
)
|
||||
const BATTERY_GAUGE_FACE_TEXTURE_PATH := (
|
||||
"res://game/presentation/cockpit/assets/battery_gauge_face_albedo_v2.png"
|
||||
)
|
||||
const HULL_GAUGE_FACE_TEXTURE_PATH := (
|
||||
"res://game/presentation/cockpit/assets/hull_gauge_face_albedo_v2.png"
|
||||
)
|
||||
const DEPTH_GAUGE_FACE_TEXTURE_PATH := (
|
||||
"res://game/presentation/cockpit/assets/depth_gauge_face_albedo_v2.png"
|
||||
)
|
||||
|
||||
var painted_steel: Material
|
||||
var dark_steel: Material
|
||||
@@ -24,21 +12,8 @@ var brushed_steel: StandardMaterial3D
|
||||
var brass: Material
|
||||
var copper: Material
|
||||
var rubber: Material
|
||||
var oxygen_gauge_face: StandardMaterial3D
|
||||
var battery_gauge_face: StandardMaterial3D
|
||||
var hull_gauge_face: StandardMaterial3D
|
||||
var depth_gauge_face: StandardMaterial3D
|
||||
var gauge_glass: StandardMaterial3D
|
||||
var needle_paint: StandardMaterial3D
|
||||
var instrument_screen: ShaderMaterial
|
||||
var warm_emission: StandardMaterial3D
|
||||
var safety_yellow: StandardMaterial3D
|
||||
var signal_red: StandardMaterial3D
|
||||
var indicator_green: StandardMaterial3D
|
||||
var indicator_amber: StandardMaterial3D
|
||||
var indicator_red: StandardMaterial3D
|
||||
var sonar_grid: StandardMaterial3D
|
||||
var warm_glass: StandardMaterial3D
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -79,21 +54,8 @@ func _init() -> void:
|
||||
0.12
|
||||
)
|
||||
rubber = _plain_material(Color(0.012, 0.018, 0.018), 0.0, 0.92)
|
||||
oxygen_gauge_face = _dedicated_gauge_material(OXYGEN_GAUGE_FACE_TEXTURE_PATH)
|
||||
battery_gauge_face = _dedicated_gauge_material(BATTERY_GAUGE_FACE_TEXTURE_PATH)
|
||||
hull_gauge_face = _dedicated_gauge_material(HULL_GAUGE_FACE_TEXTURE_PATH)
|
||||
depth_gauge_face = _dedicated_gauge_material(DEPTH_GAUGE_FACE_TEXTURE_PATH)
|
||||
gauge_glass = _glass_material()
|
||||
needle_paint = _plain_material(Color(0.68, 0.055, 0.022), 0.12, 0.44)
|
||||
instrument_screen = _screen_material()
|
||||
warm_emission = _emissive_material(Color(0.96, 0.52, 0.18), 2.4)
|
||||
safety_yellow = _plain_material(Color(0.48, 0.31, 0.035), 0.48, 0.53)
|
||||
signal_red = _plain_material(Color(0.42, 0.025, 0.015), 0.38, 0.48)
|
||||
indicator_green = _emissive_material(Color(0.05, 0.82, 0.24), 2.8)
|
||||
indicator_amber = _emissive_material(Color(1.0, 0.43, 0.035), 2.8)
|
||||
indicator_red = _emissive_material(Color(0.9, 0.045, 0.018), 2.8)
|
||||
sonar_grid = _emissive_material(Color(0.02, 0.20, 0.22), 0.55)
|
||||
warm_glass = _warm_glass_material()
|
||||
|
||||
|
||||
func _weathered_material(
|
||||
@@ -185,27 +147,6 @@ func _load_texture(resource_path: String) -> Texture2D:
|
||||
return texture
|
||||
|
||||
|
||||
func _dedicated_gauge_material(resource_path: String) -> StandardMaterial3D:
|
||||
var material := _textured_material(
|
||||
_load_texture(resource_path),
|
||||
Color(0.94, 0.92, 0.86),
|
||||
0.02,
|
||||
0.66
|
||||
)
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
return material
|
||||
|
||||
|
||||
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 _screen_material() -> ShaderMaterial:
|
||||
var shader := Shader.new()
|
||||
shader.code = """
|
||||
@@ -238,14 +179,3 @@ func _emissive_material(color: Color, energy: float) -> StandardMaterial3D:
|
||||
material.emission = color
|
||||
material.emission_energy_multiplier = energy
|
||||
return material
|
||||
|
||||
|
||||
func _warm_glass_material() -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
material.albedo_color = Color(1.0, 0.48, 0.12, 0.72)
|
||||
material.roughness = 0.18
|
||||
material.emission_enabled = true
|
||||
material.emission = Color(1.0, 0.28, 0.045)
|
||||
material.emission_energy_multiplier = 2.6
|
||||
return material
|
||||
@@ -87,3 +87,27 @@ static func add_sphere(
|
||||
instance.position = position
|
||||
parent.add_child(instance)
|
||||
return instance
|
||||
|
||||
|
||||
## Gives an imported mesh instance its own copy of its first surface material,
|
||||
## so driving a property (emission) never edits the resource shared by the model.
|
||||
static func own_surface_material(instance: MeshInstance3D) -> BaseMaterial3D:
|
||||
if instance == null or instance.mesh == null or instance.mesh.get_surface_count() == 0:
|
||||
return null
|
||||
var shared := instance.mesh.surface_get_material(0) as BaseMaterial3D
|
||||
if shared == null:
|
||||
return null
|
||||
var owned := shared.duplicate() as BaseMaterial3D
|
||||
instance.set_surface_override_material(0, owned)
|
||||
return owned
|
||||
|
||||
|
||||
## Stops every mesh under `root` (root included) from casting shadows.
|
||||
static func disable_shadow_casting(root: Node) -> void:
|
||||
if root == null:
|
||||
return
|
||||
var meshes: Array[Node] = root.find_children("*", "MeshInstance3D", true, false)
|
||||
meshes.append(root)
|
||||
for node: Node in meshes:
|
||||
if node is MeshInstance3D:
|
||||
(node as MeshInstance3D).cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
@@ -9,6 +9,28 @@ const CockpitMeshFactory = preload(
|
||||
"res://game/presentation/cockpit/cockpit_mesh_factory.gd"
|
||||
)
|
||||
|
||||
const CAGE_LAMP_SCENE = preload("res://game/presentation/cockpit/assets/models/cage_lamp.glb")
|
||||
|
||||
## Flange centre of the right-hand lamp, on the flat end cap of the pressure
|
||||
## hull beside the porthole (the left lamp mirrors x). The cap is flat there, so
|
||||
## the lamp is seated square to it, facing the cabin.
|
||||
const LAMP_MOUNT_OFFSET := Vector3(0.636, 1.106, -1.3)
|
||||
## Centre of the glass globe in the lamp model: where the light is emitted.
|
||||
const LAMP_GLOBE_CENTRE := Vector3(0.0, 0.0, 0.064)
|
||||
## Globe emission (1.5 as exported): lower, so the globe does not bloom into a blob.
|
||||
const LAMP_GLASS_ENERGY := 1.0
|
||||
## Tungsten-like caged lamps: the cabin's main light.
|
||||
const WARM_LAMP_COLOR := Color(1.0, 0.66, 0.4)
|
||||
const WARM_LAMP_ENERGY := 0.5
|
||||
## Same dimming ratio as before when the red emergency light takes over.
|
||||
const WARM_LAMP_CRITICAL_ENERGY := 0.23
|
||||
## Short range and a steep falloff: each lamp makes a pool on the wall around
|
||||
## it and leaves the lower and outer hull dark.
|
||||
const WARM_LAMP_RANGE_M := 1.6
|
||||
const WARM_LAMP_ATTENUATION := 2.0
|
||||
## Cabin parts are centimetre-scale: the default bias detaches their shadows.
|
||||
const WARM_LAMP_SHADOW_BIAS := 0.03
|
||||
|
||||
var _materials: CockpitMaterialPalette
|
||||
var _warm_lights: Array[OmniLight3D] = []
|
||||
var _emergency_light: OmniLight3D
|
||||
@@ -17,8 +39,8 @@ var _emergency_light: OmniLight3D
|
||||
func _init(materials: CockpitMaterialPalette) -> void:
|
||||
_materials = materials
|
||||
name = "PracticalLighting"
|
||||
_add_caged_lamp(-0.76, -8.0)
|
||||
_add_caged_lamp(0.76, 8.0)
|
||||
_add_caged_lamp(-1.0)
|
||||
_add_caged_lamp(1.0)
|
||||
_add_fill_light()
|
||||
_add_emergency_light()
|
||||
|
||||
@@ -26,75 +48,69 @@ func _init(materials: CockpitMaterialPalette) -> void:
|
||||
func present(alert_level: DiveAlertLevel.Value) -> void:
|
||||
_emergency_light.visible = alert_level >= DiveAlertLevel.Value.CRITICAL
|
||||
for light: OmniLight3D in _warm_lights:
|
||||
light.light_energy = 0.22 if alert_level >= DiveAlertLevel.Value.CRITICAL else 0.48
|
||||
light.light_energy = (
|
||||
WARM_LAMP_CRITICAL_ENERGY
|
||||
if alert_level >= DiveAlertLevel.Value.CRITICAL
|
||||
else WARM_LAMP_ENERGY
|
||||
)
|
||||
|
||||
|
||||
func practical_light_count() -> int:
|
||||
return _warm_lights.size()
|
||||
|
||||
|
||||
func _add_caged_lamp(x: float, roll_degrees: float) -> void:
|
||||
func _add_caged_lamp(side: float) -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "CagedLamp"
|
||||
root.position = Vector3(x, 0.88, -1.16)
|
||||
root.rotation_degrees.z = roll_degrees
|
||||
root.position = Vector3(
|
||||
side * LAMP_MOUNT_OFFSET.x,
|
||||
LAMP_MOUNT_OFFSET.y,
|
||||
LAMP_MOUNT_OFFSET.z
|
||||
)
|
||||
add_child(root)
|
||||
CockpitMeshFactory.add_box(
|
||||
"LampHousing",
|
||||
root,
|
||||
Vector3(0.0, 0.0, -0.025),
|
||||
Vector3(0.14, 0.11, 0.055),
|
||||
_materials.dark_steel
|
||||
var model := CAGE_LAMP_SCENE.instantiate() as Node3D
|
||||
model.name = "CageLampModel"
|
||||
root.add_child(model)
|
||||
# The light sits inside the cage, 3 cm from its bars: casting, the lamp's
|
||||
# own body would swallow it.
|
||||
CockpitMeshFactory.disable_shadow_casting(model)
|
||||
var glass := CockpitMeshFactory.own_surface_material(
|
||||
model.find_child("CageLamp_Glass", true, false) as MeshInstance3D
|
||||
)
|
||||
CockpitMeshFactory.add_sphere(
|
||||
"WarmBulb",
|
||||
root,
|
||||
Vector3(0.0, 0.0, 0.04),
|
||||
0.038,
|
||||
_materials.warm_glass
|
||||
)
|
||||
for x_offset: float in [-0.048, -0.024, 0.0, 0.024, 0.048]:
|
||||
CockpitMeshFactory.add_box(
|
||||
"LampGuard",
|
||||
root,
|
||||
Vector3(x_offset, 0.0, 0.095),
|
||||
Vector3(0.007, 0.10, 0.007),
|
||||
_materials.brushed_steel
|
||||
)
|
||||
for y_offset: float in [-0.04, 0.04]:
|
||||
CockpitMeshFactory.add_box(
|
||||
"LampGuardRail",
|
||||
root,
|
||||
Vector3(0.0, y_offset, 0.095),
|
||||
Vector3(0.115, 0.007, 0.007),
|
||||
_materials.brushed_steel
|
||||
)
|
||||
if glass != null:
|
||||
glass.emission_energy_multiplier = LAMP_GLASS_ENERGY
|
||||
var light := OmniLight3D.new()
|
||||
light.name = "WarmPracticalLight"
|
||||
light.position = Vector3(0.0, -0.03, 0.16)
|
||||
light.light_color = Color(1.0, 0.55, 0.26)
|
||||
light.light_energy = 0.48
|
||||
light.omni_range = 1.75
|
||||
light.shadow_enabled = false
|
||||
light.position = LAMP_GLOBE_CENTRE
|
||||
light.light_color = WARM_LAMP_COLOR
|
||||
light.light_energy = WARM_LAMP_ENERGY
|
||||
light.omni_range = WARM_LAMP_RANGE_M
|
||||
light.omni_attenuation = WARM_LAMP_ATTENUATION
|
||||
light.shadow_enabled = true
|
||||
light.shadow_bias = WARM_LAMP_SHADOW_BIAS
|
||||
root.add_child(light)
|
||||
_warm_lights.append(light)
|
||||
|
||||
|
||||
func _add_fill_light() -> void:
|
||||
var cool_fill := OmniLight3D.new()
|
||||
cool_fill.name = "CoolRearFill"
|
||||
cool_fill.position = Vector3(0.0, 0.42, 0.35)
|
||||
cool_fill.light_color = Color(0.24, 0.46, 0.47)
|
||||
cool_fill.light_energy = 0.36
|
||||
cool_fill.omni_range = 3.0
|
||||
cool_fill.shadow_enabled = false
|
||||
add_child(cool_fill)
|
||||
# The only cool light in the cabin: sea light entering through the porthole,
|
||||
# placed just inside the glass so it rims the ring, its bolts and the first
|
||||
# band of wall, and reaches the dashboard only from behind.
|
||||
var porthole_fill := OmniLight3D.new()
|
||||
porthole_fill.name = "PortholeFill"
|
||||
porthole_fill.position = Vector3(0.0, 0.47, -1.2)
|
||||
porthole_fill.light_color = Color(0.30, 0.56, 0.78)
|
||||
porthole_fill.light_energy = 1.0
|
||||
porthole_fill.omni_range = 1.1
|
||||
porthole_fill.shadow_enabled = false
|
||||
add_child(porthole_fill)
|
||||
|
||||
|
||||
func _add_emergency_light() -> void:
|
||||
_emergency_light = OmniLight3D.new()
|
||||
_emergency_light.name = "EmergencyLight"
|
||||
_emergency_light.position = Vector3(-0.58, 0.44, -0.83)
|
||||
# High and central, so no single dial sits right under it and washes out.
|
||||
_emergency_light.position = Vector3(0.0, 1.1, -0.95)
|
||||
_emergency_light.light_color = Color(1.0, 0.04, 0.015)
|
||||
_emergency_light.light_energy = 1.4
|
||||
_emergency_light.omni_range = 2.4
|
||||
|
||||
@@ -1,192 +1,57 @@
|
||||
class_name CockpitPressureBulkhead
|
||||
extends Node3D
|
||||
|
||||
const CockpitMaterialPalette = preload(
|
||||
"res://game/presentation/cockpit/cockpit_material_palette.gd"
|
||||
)
|
||||
const CockpitMeshFactory = preload(
|
||||
"res://game/presentation/cockpit/cockpit_mesh_factory.gd"
|
||||
)
|
||||
## Imported pressure hull (end cap, knuckle and barrel in one shell) and the
|
||||
## porthole bolted into it. Both models are instanced by `cockpit_interior.tscn`.
|
||||
|
||||
const PANEL_COUNT := 1
|
||||
const FASTENER_COUNT := 16
|
||||
const ANGULAR_STEPS := 64
|
||||
const RADIAL_STEPS := 4
|
||||
const INNER_RADIUS := 0.72
|
||||
const OUTER_RADIUS := 1.11
|
||||
const WHITEBOX_NAMES: Array[String] = [
|
||||
"LeftBulkhead",
|
||||
"RightBulkhead",
|
||||
"TopBulkhead",
|
||||
"BottomBulkhead",
|
||||
]
|
||||
|
||||
var _materials: CockpitMaterialPalette
|
||||
var _panel_count := 0
|
||||
var _hull_shells: Array[Node] = []
|
||||
var _porthole_frame: MeshInstance3D
|
||||
var _porthole_glass: MeshInstance3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_materials = CockpitMaterialPalette.new()
|
||||
_hide_rectangular_whitebox()
|
||||
_style_existing_frame()
|
||||
_build_curved_panels()
|
||||
_build_outer_rib()
|
||||
_build_fasteners()
|
||||
_build_clamp_plates()
|
||||
var hull := get_node_or_null("HullWall")
|
||||
if hull != null:
|
||||
_hull_shells = hull.find_children("*", "MeshInstance3D", true, false)
|
||||
_porthole_frame = _find_mesh("Porthole")
|
||||
_porthole_glass = _find_mesh("PortholeGlass")
|
||||
if _hull_shells.is_empty() or _porthole_frame == null or _porthole_glass == null:
|
||||
push_error("Pressure hull or porthole model is missing a node")
|
||||
return
|
||||
_porthole_glass.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
|
||||
|
||||
func panel_count() -> int:
|
||||
return _panel_count
|
||||
return _hull_shells.size()
|
||||
|
||||
|
||||
func hull_shell() -> MeshInstance3D:
|
||||
return null if _hull_shells.is_empty() else _hull_shells[0] as MeshInstance3D
|
||||
|
||||
|
||||
func porthole_frame() -> MeshInstance3D:
|
||||
return _porthole_frame
|
||||
|
||||
|
||||
func porthole_glass() -> MeshInstance3D:
|
||||
return _porthole_glass
|
||||
|
||||
|
||||
func rectangular_whitebox_is_hidden() -> bool:
|
||||
for node_name: String in ["LeftBulkhead", "RightBulkhead", "TopBulkhead", "BottomBulkhead"]:
|
||||
for node_name: String in WHITEBOX_NAMES:
|
||||
var whitebox := get_parent().get_node_or_null(node_name) as MeshInstance3D
|
||||
if whitebox != null and whitebox.visible:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _hide_rectangular_whitebox() -> void:
|
||||
for node_name: String in ["LeftBulkhead", "RightBulkhead", "TopBulkhead", "BottomBulkhead"]:
|
||||
var whitebox := get_parent().get_node_or_null(node_name) as MeshInstance3D
|
||||
if whitebox != null:
|
||||
whitebox.visible = false
|
||||
|
||||
|
||||
func _style_existing_frame() -> void:
|
||||
var frame := get_parent().get_node_or_null("WindowFrame") as MeshInstance3D
|
||||
if frame != null:
|
||||
frame.material_override = _materials.brushed_steel
|
||||
for child: Node in get_parent().get_children():
|
||||
if child is MeshInstance3D and child.name.begins_with("Bolt"):
|
||||
(child as MeshInstance3D).material_override = _materials.brass
|
||||
|
||||
|
||||
func _build_curved_panels() -> void:
|
||||
var panel_angle := TAU / float(PANEL_COUNT)
|
||||
for panel_index in PANEL_COUNT:
|
||||
var start_angle := panel_index * panel_angle
|
||||
var end_angle := (panel_index + 1) * panel_angle
|
||||
var panel := MeshInstance3D.new()
|
||||
panel.name = "PressurePanel%02d" % (panel_index + 1)
|
||||
panel.mesh = _create_curved_panel_mesh(start_angle, end_angle)
|
||||
panel.material_override = _materials.overhead_steel
|
||||
panel.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
add_child(panel)
|
||||
_panel_count += 1
|
||||
|
||||
|
||||
func _create_curved_panel_mesh(start_angle: float, end_angle: float) -> ArrayMesh:
|
||||
var surface := SurfaceTool.new()
|
||||
surface.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
for angular_index in ANGULAR_STEPS:
|
||||
var angle_ratio_0 := float(angular_index) / float(ANGULAR_STEPS)
|
||||
var angle_ratio_1 := float(angular_index + 1) / float(ANGULAR_STEPS)
|
||||
var angle_0 := lerpf(start_angle, end_angle, angle_ratio_0)
|
||||
var angle_1 := lerpf(start_angle, end_angle, angle_ratio_1)
|
||||
for radial_index in RADIAL_STEPS:
|
||||
var radial_ratio_0 := float(radial_index) / float(RADIAL_STEPS)
|
||||
var radial_ratio_1 := float(radial_index + 1) / float(RADIAL_STEPS)
|
||||
_add_panel_quad(
|
||||
surface,
|
||||
angle_0,
|
||||
angle_1,
|
||||
angle_ratio_0,
|
||||
angle_ratio_1,
|
||||
radial_ratio_0,
|
||||
radial_ratio_1
|
||||
)
|
||||
surface.generate_normals()
|
||||
return surface.commit()
|
||||
|
||||
|
||||
func _add_panel_quad(
|
||||
surface: SurfaceTool,
|
||||
angle_0: float,
|
||||
angle_1: float,
|
||||
angle_ratio_0: float,
|
||||
angle_ratio_1: float,
|
||||
radial_ratio_0: float,
|
||||
radial_ratio_1: float
|
||||
) -> void:
|
||||
_add_panel_vertex(surface, angle_0, angle_ratio_0, radial_ratio_0)
|
||||
_add_panel_vertex(surface, angle_1, angle_ratio_1, radial_ratio_0)
|
||||
_add_panel_vertex(surface, angle_0, angle_ratio_0, radial_ratio_1)
|
||||
_add_panel_vertex(surface, angle_0, angle_ratio_0, radial_ratio_1)
|
||||
_add_panel_vertex(surface, angle_1, angle_ratio_1, radial_ratio_0)
|
||||
_add_panel_vertex(surface, angle_1, angle_ratio_1, radial_ratio_1)
|
||||
|
||||
|
||||
func _add_panel_vertex(
|
||||
surface: SurfaceTool,
|
||||
angle: float,
|
||||
angle_ratio: float,
|
||||
radial_ratio: float
|
||||
) -> void:
|
||||
var radius := lerpf(INNER_RADIUS, OUTER_RADIUS, radial_ratio)
|
||||
var convex_depth := sin(radial_ratio * PI) * 0.075
|
||||
surface.set_uv(Vector2(angle_ratio, radial_ratio))
|
||||
surface.add_vertex(
|
||||
Vector3(cos(angle) * radius, sin(angle) * radius, 0.045 + convex_depth)
|
||||
)
|
||||
|
||||
|
||||
func _build_outer_rib() -> void:
|
||||
var rib := MeshInstance3D.new()
|
||||
rib.name = "OuterPressureRib"
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = 1.055
|
||||
torus.outer_radius = 1.125
|
||||
torus.rings = 64
|
||||
torus.ring_segments = 12
|
||||
rib.mesh = torus
|
||||
rib.material_override = _materials.dark_steel
|
||||
rib.rotation_degrees.x = 90.0
|
||||
rib.position.z = 0.025
|
||||
add_child(rib)
|
||||
var inner_rib := MeshInstance3D.new()
|
||||
inner_rib.name = "InnerPressureRib"
|
||||
var inner_torus := TorusMesh.new()
|
||||
inner_torus.inner_radius = 0.695
|
||||
inner_torus.outer_radius = 0.755
|
||||
inner_torus.rings = 64
|
||||
inner_torus.ring_segments = 12
|
||||
inner_rib.mesh = inner_torus
|
||||
inner_rib.material_override = _materials.brushed_steel
|
||||
inner_rib.rotation_degrees.x = 90.0
|
||||
inner_rib.position.z = 0.075
|
||||
add_child(inner_rib)
|
||||
|
||||
|
||||
func _build_fasteners() -> void:
|
||||
for fastener_index in FASTENER_COUNT:
|
||||
var angle := (fastener_index + 0.5) * TAU / float(FASTENER_COUNT)
|
||||
var fastener_position := Vector3(
|
||||
cos(angle) * 0.96,
|
||||
sin(angle) * 0.96,
|
||||
0.12
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"OuterFastener",
|
||||
self,
|
||||
fastener_position,
|
||||
0.020,
|
||||
0.018,
|
||||
_materials.brass,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
CockpitMeshFactory.add_sphere(
|
||||
"FastenerCap",
|
||||
self,
|
||||
fastener_position + Vector3(0.0, 0.0, 0.012),
|
||||
0.017,
|
||||
_materials.brushed_steel
|
||||
)
|
||||
|
||||
|
||||
func _build_clamp_plates() -> void:
|
||||
for plate_index in 12:
|
||||
var angle := (plate_index + 0.5) * TAU / 12.0
|
||||
var plate := CockpitMeshFactory.add_box(
|
||||
"PressureClamp",
|
||||
self,
|
||||
Vector3(cos(angle) * 0.88, sin(angle) * 0.88, 0.105),
|
||||
Vector3(0.11, 0.055, 0.028),
|
||||
_materials.brushed_steel
|
||||
)
|
||||
plate.rotation_degrees.z = rad_to_deg(angle) + 90.0
|
||||
func _find_mesh(mesh_name: String) -> MeshInstance3D:
|
||||
var matches := find_children(mesh_name, "MeshInstance3D", true, false)
|
||||
return null if matches.is_empty() else matches[0] as MeshInstance3D
|
||||
@@ -9,7 +9,6 @@ const CockpitMeshFactory = preload(
|
||||
)
|
||||
|
||||
var _materials: CockpitMaterialPalette
|
||||
var _tank_count := 0
|
||||
var _junction_box_count := 0
|
||||
var _control_count := 0
|
||||
|
||||
@@ -17,14 +16,7 @@ var _control_count := 0
|
||||
func _init(materials: CockpitMaterialPalette) -> void:
|
||||
_materials = materials
|
||||
name = "SideEquipment"
|
||||
_build_left_life_support_bank()
|
||||
_build_right_electrical_bank()
|
||||
_build_cable_looms()
|
||||
_build_control_rails()
|
||||
|
||||
|
||||
func tank_count() -> int:
|
||||
return _tank_count
|
||||
|
||||
|
||||
func junction_box_count() -> int:
|
||||
@@ -35,87 +27,11 @@ func control_count() -> int:
|
||||
return _control_count
|
||||
|
||||
|
||||
func _build_left_life_support_bank() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "LifeSupportBank"
|
||||
add_child(root)
|
||||
for tank_data: Vector3 in [
|
||||
Vector3(-0.78, 0.55, -1.17),
|
||||
Vector3(-0.68, 0.47, -1.19),
|
||||
]:
|
||||
_add_tank(root, tank_data)
|
||||
|
||||
_add_service_box(root, Vector3(-0.61, 0.42, -1.14), "O₂", -10.0)
|
||||
_add_indicator_column(root, Vector3(-0.55, 0.39, -1.055))
|
||||
_add_valve_wheel(root, Vector3(-0.66, 0.12, -1.08))
|
||||
|
||||
CockpitMeshFactory.add_cylinder_between(
|
||||
"ManifoldPipe",
|
||||
root,
|
||||
Vector3(-0.78, 0.24, -1.13),
|
||||
Vector3(-0.67, 0.09, -1.11),
|
||||
0.018,
|
||||
_materials.copper
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder_between(
|
||||
"ManifoldReturn",
|
||||
root,
|
||||
Vector3(-0.68, 0.25, -1.15),
|
||||
Vector3(-0.59, 0.02, -1.10),
|
||||
0.014,
|
||||
_materials.brass
|
||||
)
|
||||
|
||||
|
||||
func _add_tank(root: Node3D, tank_position: Vector3) -> void:
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"OxygenTank",
|
||||
root,
|
||||
tank_position,
|
||||
0.055,
|
||||
0.43,
|
||||
_materials.safety_yellow
|
||||
)
|
||||
CockpitMeshFactory.add_sphere(
|
||||
"TankTop",
|
||||
root,
|
||||
tank_position + Vector3(0.0, 0.215, 0.0),
|
||||
0.055,
|
||||
_materials.safety_yellow
|
||||
)
|
||||
CockpitMeshFactory.add_sphere(
|
||||
"TankBottom",
|
||||
root,
|
||||
tank_position - Vector3(0.0, 0.215, 0.0),
|
||||
0.055,
|
||||
_materials.safety_yellow
|
||||
)
|
||||
for offset_y: float in [-0.13, 0.13]:
|
||||
var strap := MeshInstance3D.new()
|
||||
strap.name = "TankStrap"
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = 0.054
|
||||
torus.outer_radius = 0.066
|
||||
torus.rings = 20
|
||||
torus.ring_segments = 6
|
||||
strap.mesh = torus
|
||||
strap.material_override = _materials.dark_steel
|
||||
strap.position = tank_position + Vector3(0.0, offset_y, 0.0)
|
||||
root.add_child(strap)
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"TankValve",
|
||||
root,
|
||||
tank_position + Vector3(0.0, 0.29, 0.0),
|
||||
0.017,
|
||||
0.075,
|
||||
_materials.brass
|
||||
)
|
||||
_tank_count += 1
|
||||
|
||||
|
||||
func _build_right_electrical_bank() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "ElectricalBank"
|
||||
# Seated on the flat end cap of the pressure hull, behind the sonar.
|
||||
root.position = Vector3(0.14, -0.18, -0.055)
|
||||
add_child(root)
|
||||
_add_service_box(root, Vector3(0.72, 0.65, -1.17), "ELEC", 11.0)
|
||||
_add_service_box(root, Vector3(0.73, 0.36, -1.16), "POMPES", 8.0)
|
||||
@@ -123,17 +39,6 @@ func _build_right_electrical_bank() -> void:
|
||||
_add_vent(root, Vector3(0.715, 0.40, -1.085))
|
||||
_add_knob(root, Vector3(0.66, 0.55, -1.08))
|
||||
_add_knob(root, Vector3(0.68, 0.25, -1.07))
|
||||
_add_indicator_column(root, Vector3(0.795, 0.49, -1.075))
|
||||
|
||||
for pipe_x: float in [0.64, 0.82]:
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"ElectricalConduit",
|
||||
root,
|
||||
Vector3(pipe_x, 0.70, -1.20),
|
||||
0.017,
|
||||
0.58,
|
||||
_materials.copper
|
||||
)
|
||||
|
||||
|
||||
func _add_service_box(
|
||||
@@ -198,123 +103,3 @@ func _add_knob(root: Node3D, knob_position: Vector3) -> void:
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
_control_count += 1
|
||||
|
||||
|
||||
func _add_indicator_column(root: Node3D, center: Vector3) -> void:
|
||||
var indicator_materials: Array[StandardMaterial3D] = [
|
||||
_materials.indicator_green,
|
||||
_materials.indicator_amber,
|
||||
_materials.indicator_red,
|
||||
]
|
||||
for index in 3:
|
||||
CockpitMeshFactory.add_box(
|
||||
"PilotLightBezel",
|
||||
root,
|
||||
center + Vector3(0.0, (1 - index) * 0.055, -0.004),
|
||||
Vector3(0.038, 0.038, 0.018),
|
||||
_materials.dark_steel
|
||||
)
|
||||
CockpitMeshFactory.add_sphere(
|
||||
"PilotLight",
|
||||
root,
|
||||
center + Vector3(0.0, (1 - index) * 0.055, 0.010),
|
||||
0.013,
|
||||
indicator_materials[index]
|
||||
)
|
||||
_control_count += 1
|
||||
|
||||
|
||||
func _add_valve_wheel(root: Node3D, wheel_position: Vector3) -> void:
|
||||
var wheel := MeshInstance3D.new()
|
||||
wheel.name = "ValveWheel"
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = 0.034
|
||||
torus.outer_radius = 0.046
|
||||
torus.rings = 20
|
||||
torus.ring_segments = 7
|
||||
wheel.mesh = torus
|
||||
wheel.material_override = _materials.signal_red
|
||||
wheel.position = wheel_position
|
||||
wheel.rotation_degrees.x = 90.0
|
||||
root.add_child(wheel)
|
||||
for angle_degrees: float in [0.0, 45.0, 90.0, 135.0]:
|
||||
CockpitMeshFactory.add_box(
|
||||
"ValveSpoke",
|
||||
root,
|
||||
wheel_position + Vector3(0.0, 0.0, 0.002),
|
||||
Vector3(0.075, 0.007, 0.008),
|
||||
_materials.signal_red,
|
||||
Vector3(0.0, 0.0, angle_degrees)
|
||||
)
|
||||
_control_count += 1
|
||||
|
||||
|
||||
func _build_cable_looms() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "CableLooms"
|
||||
add_child(root)
|
||||
var left_points: Array[Vector3] = [
|
||||
Vector3(-0.82, 0.93, -1.15),
|
||||
Vector3(-0.72, 0.87, -1.16),
|
||||
Vector3(-0.64, 0.73, -1.14),
|
||||
Vector3(-0.61, 0.58, -1.12),
|
||||
]
|
||||
var right_points: Array[Vector3] = [
|
||||
Vector3(0.84, 0.91, -1.15),
|
||||
Vector3(0.75, 0.86, -1.16),
|
||||
Vector3(0.68, 0.73, -1.14),
|
||||
Vector3(0.65, 0.59, -1.12),
|
||||
]
|
||||
_add_cable_path(root, left_points)
|
||||
_add_cable_path(root, right_points)
|
||||
|
||||
|
||||
func _add_cable_path(root: Node3D, points: Array[Vector3]) -> void:
|
||||
for point_index in points.size() - 1:
|
||||
CockpitMeshFactory.add_cylinder_between(
|
||||
"ArmouredCable",
|
||||
root,
|
||||
points[point_index],
|
||||
points[point_index + 1],
|
||||
0.012,
|
||||
_materials.rubber
|
||||
)
|
||||
|
||||
|
||||
func _build_control_rails() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "ControlRails"
|
||||
add_child(root)
|
||||
CockpitMeshFactory.add_cylinder_between(
|
||||
"LowerSteelRail",
|
||||
root,
|
||||
Vector3(-0.82, -0.34, -1.12),
|
||||
Vector3(0.82, -0.34, -1.12),
|
||||
0.026,
|
||||
_materials.brushed_steel
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder_between(
|
||||
"LeftGrip",
|
||||
root,
|
||||
Vector3(-0.58, -0.34, -1.11),
|
||||
Vector3(-0.18, -0.34, -1.11),
|
||||
0.034,
|
||||
_materials.rubber
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder_between(
|
||||
"RightGrip",
|
||||
root,
|
||||
Vector3(0.18, -0.34, -1.11),
|
||||
Vector3(0.58, -0.34, -1.11),
|
||||
0.034,
|
||||
_materials.rubber
|
||||
)
|
||||
for x: float in [-0.82, 0.82]:
|
||||
CockpitMeshFactory.add_cylinder_between(
|
||||
"RailSupport",
|
||||
root,
|
||||
Vector3(x, -0.34, -1.12),
|
||||
Vector3(x, -0.20, -1.08),
|
||||
0.022,
|
||||
_materials.brushed_steel
|
||||
)
|
||||
@@ -1,145 +1,36 @@
|
||||
class_name CockpitSonarConsole
|
||||
extends Node3D
|
||||
|
||||
const CockpitMaterialPalette = preload(
|
||||
"res://game/presentation/cockpit/cockpit_material_palette.gd"
|
||||
)
|
||||
const CockpitMeshFactory = preload(
|
||||
"res://game/presentation/cockpit/cockpit_mesh_factory.gd"
|
||||
)
|
||||
## Imported sonar monitor. Exposes its glass as the cockpit's `%SonarScreen` so
|
||||
## the instrument panel can put the shared sonar texture on it.
|
||||
|
||||
var _materials: CockpitMaterialPalette
|
||||
var _control_count := 0
|
||||
## Lamps and knobs are modelled into the housing mesh, not separate nodes:
|
||||
## three amber push-buttons and two knurled knobs (model contract).
|
||||
const HOUSING_CONTROL_COUNT := 5
|
||||
|
||||
var _housing: MeshInstance3D
|
||||
var _screen: MeshInstance3D
|
||||
|
||||
|
||||
func _init(materials: CockpitMaterialPalette) -> void:
|
||||
_materials = materials
|
||||
name = "SonarConsoleDetailing"
|
||||
_build_bezel()
|
||||
_build_grid()
|
||||
_build_control_strip()
|
||||
_build_mounting_hardware()
|
||||
func _ready() -> void:
|
||||
_housing = find_child("Sonar", true, false) as MeshInstance3D
|
||||
_screen = find_child("SonarScreen", true, false) as MeshInstance3D
|
||||
if _housing == null or _screen == null:
|
||||
push_error("Sonar model is missing its Sonar or SonarScreen node")
|
||||
return
|
||||
# The glass belongs to the imported scene; adopt it so the cockpit scene
|
||||
# can address it by unique name like any other instrument.
|
||||
_screen.owner = owner
|
||||
_screen.unique_name_in_owner = true
|
||||
|
||||
|
||||
func housing() -> MeshInstance3D:
|
||||
return _housing
|
||||
|
||||
|
||||
func screen() -> MeshInstance3D:
|
||||
return _screen
|
||||
|
||||
|
||||
func control_count() -> int:
|
||||
return _control_count
|
||||
|
||||
|
||||
func _build_bezel() -> void:
|
||||
CockpitMeshFactory.add_box(
|
||||
"UpperBezel",
|
||||
self,
|
||||
Vector3(-0.025, 0.151, 0.073),
|
||||
Vector3(0.41, 0.034, 0.035),
|
||||
_materials.brushed_steel
|
||||
)
|
||||
CockpitMeshFactory.add_box(
|
||||
"LowerBezel",
|
||||
self,
|
||||
Vector3(-0.025, -0.126, 0.073),
|
||||
Vector3(0.41, 0.034, 0.035),
|
||||
_materials.brushed_steel
|
||||
)
|
||||
for x: float in [-0.225, 0.175]:
|
||||
CockpitMeshFactory.add_box(
|
||||
"SideBezel",
|
||||
self,
|
||||
Vector3(x, 0.012, 0.073),
|
||||
Vector3(0.035, 0.31, 0.035),
|
||||
_materials.brushed_steel
|
||||
)
|
||||
var label := Label3D.new()
|
||||
label.name = "SonarPlate"
|
||||
label.text = "SONAR · BATHYMÉTRIE"
|
||||
label.font_size = 12
|
||||
label.pixel_size = 0.00062
|
||||
label.modulate = Color(0.7, 0.73, 0.64)
|
||||
label.position = Vector3(-0.04, 0.156, 0.097)
|
||||
add_child(label)
|
||||
|
||||
|
||||
func _build_control_strip() -> void:
|
||||
CockpitMeshFactory.add_box(
|
||||
"ControlStrip",
|
||||
self,
|
||||
Vector3(0.235, 0.012, 0.072),
|
||||
Vector3(0.075, 0.31, 0.04),
|
||||
_materials.dark_steel
|
||||
)
|
||||
var lamps: Array[StandardMaterial3D] = [
|
||||
_materials.indicator_amber,
|
||||
_materials.indicator_amber,
|
||||
_materials.indicator_green,
|
||||
]
|
||||
for index in 3:
|
||||
var y := 0.095 - index * 0.073
|
||||
CockpitMeshFactory.add_box(
|
||||
"SonarLampBezel",
|
||||
self,
|
||||
Vector3(0.235, y, 0.101),
|
||||
Vector3(0.044, 0.044, 0.018),
|
||||
_materials.brass
|
||||
)
|
||||
CockpitMeshFactory.add_sphere(
|
||||
"SonarLamp",
|
||||
self,
|
||||
Vector3(0.235, y, 0.116),
|
||||
0.013,
|
||||
lamps[index]
|
||||
)
|
||||
_control_count += 1
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"SonarGainKnob",
|
||||
self,
|
||||
Vector3(0.235, -0.102, 0.108),
|
||||
0.024,
|
||||
0.026,
|
||||
_materials.rubber,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
_control_count += 1
|
||||
|
||||
|
||||
func _build_grid() -> void:
|
||||
for x: float in [-0.145, -0.085, -0.025, 0.035, 0.095]:
|
||||
CockpitMeshFactory.add_box(
|
||||
"SonarGridVertical",
|
||||
self,
|
||||
Vector3(x, 0.012, 0.076),
|
||||
Vector3(0.0015, 0.215, 0.002),
|
||||
_materials.sonar_grid
|
||||
)
|
||||
for y: float in [-0.075, -0.025, 0.025, 0.075]:
|
||||
CockpitMeshFactory.add_box(
|
||||
"SonarGridHorizontal",
|
||||
self,
|
||||
Vector3(-0.025, y, 0.076),
|
||||
Vector3(0.33, 0.0015, 0.002),
|
||||
_materials.sonar_grid
|
||||
)
|
||||
|
||||
|
||||
func _build_mounting_hardware() -> void:
|
||||
for corner: Vector2 in [
|
||||
Vector2(-0.205, -0.105),
|
||||
Vector2(-0.205, 0.13),
|
||||
Vector2(0.195, -0.105),
|
||||
Vector2(0.195, 0.13),
|
||||
]:
|
||||
CockpitMeshFactory.add_cylinder(
|
||||
"SonarFastener",
|
||||
self,
|
||||
Vector3(corner.x, corner.y, 0.103),
|
||||
0.008,
|
||||
0.012,
|
||||
_materials.brass,
|
||||
Vector3(90.0, 0.0, 0.0)
|
||||
)
|
||||
CockpitMeshFactory.add_cylinder_between(
|
||||
"LeftSonarHandle",
|
||||
self,
|
||||
Vector3(-0.255, -0.09, 0.06),
|
||||
Vector3(-0.255, 0.11, 0.06),
|
||||
0.013,
|
||||
_materials.brass
|
||||
)
|
||||
return 0 if _housing == null else HOUSING_CONTROL_COUNT
|
||||
@@ -0,0 +1,26 @@
|
||||
class_name CockpitSwitchPanel
|
||||
extends Node3D
|
||||
|
||||
## Imported switch panel. Decorative in this slice: every control stays in its
|
||||
## exported rest pose and no input reaches it.
|
||||
|
||||
const CONTROL_PREFIXES: Array[String] = ["Toggle_", "Rocker_", "Cover_", "Lever_"]
|
||||
|
||||
var _controls: Array[Node3D] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for prefix: String in CONTROL_PREFIXES:
|
||||
for control: Node in find_children("%s*" % prefix, "Node3D", true, false):
|
||||
_controls.append(control as Node3D)
|
||||
|
||||
|
||||
func control_count() -> int:
|
||||
return _controls.size()
|
||||
|
||||
|
||||
func controls_are_at_rest() -> bool:
|
||||
for control: Node3D in _controls:
|
||||
if not control.rotation.is_zero_approx():
|
||||
return false
|
||||
return true
|
||||
@@ -61,6 +61,10 @@ func built_grid_segment_count() -> int:
|
||||
return _renderer.built_grid_segment_count()
|
||||
|
||||
|
||||
func depth_scale_texts() -> PackedStringArray:
|
||||
return _renderer.depth_scale_texts()
|
||||
|
||||
|
||||
func idle_prompt_visible() -> bool:
|
||||
return _renderer.idle_prompt_visible()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ script = ExtResource("1_display")
|
||||
|
||||
[node name="SonarReliefRenderer" type="SubViewport" parent="."]
|
||||
unique_name_in_owner = true
|
||||
size = Vector2i(960, 540)
|
||||
size = Vector2i(960, 604)
|
||||
render_target_update_mode = 4
|
||||
script = ExtResource("2_renderer")
|
||||
|
||||
@@ -77,4 +77,4 @@ text = "SONAR PRÊT · F POUR ÉMETTRE"
|
||||
[node name="Legend" type="Label" parent="ExpandedPanel/Layout"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.58, 0.7, 0.71, 1)
|
||||
text = "BLEU PROFOND · VERT/JAUNE HAUTEUR · ORANGE ANOMALIE · NOIR INCONNU · TAB FERMER"
|
||||
text = "TURQUOISE SOMBRE PROFOND · CYAN CLAIR HAUTEUR · ORANGE ANOMALIE · NOIR INCONNU · TAB FERMER"
|
||||
@@ -5,53 +5,59 @@ const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
||||
const SonarSample = preload("res://game/domain/sonar/sonar_sample.gd")
|
||||
const SonarScan = preload("res://game/domain/sonar/sonar_scan.gd")
|
||||
|
||||
const COLOR_DEEP := Color("173f91")
|
||||
const COLOR_LOW := Color("168fc1")
|
||||
const COLOR_MIDDLE := Color("36b979")
|
||||
const COLOR_HIGH := Color("d7d93d")
|
||||
const COLOR_PEAK := Color("ee5734")
|
||||
const COLOR_METAL := Color("ff6b22")
|
||||
## Grid lines are drawn by the relief shader on the cell edges: every built
|
||||
## cell carries one row edge and one column edge of the grid.
|
||||
const GRID_SEGMENTS_PER_CELL := 2
|
||||
## Two triangles per cell, wound like the sample grid.
|
||||
const CELL_CORNER_ORDER: Array[int] = [0, 2, 1, 1, 2, 3]
|
||||
|
||||
|
||||
static func build_full_mesh(
|
||||
scan: SonarScan,
|
||||
material: StandardMaterial3D
|
||||
) -> ArrayMesh:
|
||||
static func build_full_mesh(scan: SonarScan, material: Material) -> ArrayMesh:
|
||||
var mesh := ArrayMesh.new()
|
||||
for row in range(1, scan.samples.rows):
|
||||
add_row_band(mesh, scan, row, material)
|
||||
return mesh
|
||||
|
||||
|
||||
## Adds the cells between `row - 1` and `row`; returns the triangle count.
|
||||
## Per vertex: UV = (column, row) of the sample grid, COLOR.r = height relative
|
||||
## to the scan (0 low, 1 peak), COLOR.g = 1 on metal, COLOR.b = echo intensity.
|
||||
static func add_row_band(
|
||||
mesh: ArrayMesh,
|
||||
scan: SonarScan,
|
||||
row: int,
|
||||
material: StandardMaterial3D
|
||||
material: Material
|
||||
) -> int:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var colors := PackedColorArray()
|
||||
var uvs := PackedVector2Array()
|
||||
for column in range(scan.samples.columns - 1):
|
||||
var top_left: SonarSample = scan.samples.sample_at(row - 1, column)
|
||||
var top_right: SonarSample = scan.samples.sample_at(row - 1, column + 1)
|
||||
var bottom_left: SonarSample = scan.samples.sample_at(row, column)
|
||||
var bottom_right: SonarSample = scan.samples.sample_at(row, column + 1)
|
||||
var corners: Array[SonarSample] = [
|
||||
scan.samples.sample_at(row - 1, column),
|
||||
scan.samples.sample_at(row - 1, column + 1),
|
||||
scan.samples.sample_at(row, column),
|
||||
scan.samples.sample_at(row, column + 1),
|
||||
]
|
||||
if not (
|
||||
top_left.observed
|
||||
and top_right.observed
|
||||
and bottom_left.observed
|
||||
and bottom_right.observed
|
||||
corners[0].observed
|
||||
and corners[1].observed
|
||||
and corners[2].observed
|
||||
and corners[3].observed
|
||||
):
|
||||
continue
|
||||
_append_triangle(
|
||||
vertices, normals, colors, scan,
|
||||
top_left, bottom_left, top_right
|
||||
)
|
||||
_append_triangle(
|
||||
vertices, normals, colors, scan,
|
||||
top_right, bottom_left, bottom_right
|
||||
)
|
||||
var grid: Array[Vector2] = [
|
||||
Vector2(column, row - 1),
|
||||
Vector2(column + 1, row - 1),
|
||||
Vector2(column, row),
|
||||
Vector2(column + 1, row),
|
||||
]
|
||||
for corner: int in CELL_CORNER_ORDER:
|
||||
var sample := corners[corner]
|
||||
vertices.append(sample.position_m)
|
||||
normals.append(sample.normal)
|
||||
colors.append(_sample_data(scan, sample))
|
||||
uvs.append(grid[corner])
|
||||
if vertices.is_empty():
|
||||
return 0
|
||||
var arrays: Array = []
|
||||
@@ -59,93 +65,21 @@ static func add_row_band(
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
arrays[Mesh.ARRAY_COLOR] = colors
|
||||
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
mesh.surface_set_material(mesh.get_surface_count() - 1, material)
|
||||
return vertices.size() / 3
|
||||
|
||||
|
||||
static func add_grid_band(
|
||||
mesh: ArrayMesh,
|
||||
scan: SonarScan,
|
||||
row: int,
|
||||
material: StandardMaterial3D
|
||||
) -> int:
|
||||
var vertices := PackedVector3Array()
|
||||
for column in scan.samples.columns:
|
||||
if column % 6 != 0 and column != scan.samples.columns - 1:
|
||||
continue
|
||||
_append_grid_segment(
|
||||
vertices,
|
||||
scan.samples.sample_at(row - 1, column),
|
||||
scan.samples.sample_at(row, column)
|
||||
)
|
||||
if row == 1 or row % 4 == 0 or row == scan.samples.rows - 1:
|
||||
for column in range(scan.samples.columns - 1):
|
||||
_append_grid_segment(
|
||||
vertices,
|
||||
scan.samples.sample_at(row, column),
|
||||
scan.samples.sample_at(row, column + 1)
|
||||
)
|
||||
if vertices.is_empty():
|
||||
return 0
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays)
|
||||
mesh.surface_set_material(mesh.get_surface_count() - 1, material)
|
||||
return vertices.size() / 2
|
||||
|
||||
|
||||
static func _append_grid_segment(
|
||||
vertices: PackedVector3Array,
|
||||
a: SonarSample,
|
||||
b: SonarSample
|
||||
) -> void:
|
||||
if not a.observed or not b.observed:
|
||||
return
|
||||
vertices.append(a.position_m + a.normal * 0.08)
|
||||
vertices.append(b.position_m + b.normal * 0.08)
|
||||
|
||||
|
||||
static func _append_triangle(
|
||||
vertices: PackedVector3Array,
|
||||
normals: PackedVector3Array,
|
||||
colors: PackedColorArray,
|
||||
scan: SonarScan,
|
||||
a: SonarSample,
|
||||
b: SonarSample,
|
||||
c: SonarSample
|
||||
) -> void:
|
||||
for sample: SonarSample in [a, b, c]:
|
||||
vertices.append(sample.position_m)
|
||||
normals.append(sample.normal)
|
||||
colors.append(_sample_color(scan, sample))
|
||||
|
||||
|
||||
static func _sample_color(scan: SonarScan, sample: SonarSample) -> Color:
|
||||
if sample.material == SonarMaterial.Value.METAL:
|
||||
return COLOR_METAL
|
||||
var height := inverse_lerp(
|
||||
scan.minimum_elevation_m,
|
||||
scan.maximum_elevation_m,
|
||||
sample.elevation_m
|
||||
)
|
||||
var color: Color
|
||||
if height <= 0.28:
|
||||
color = COLOR_DEEP.lerp(COLOR_LOW, height / 0.28)
|
||||
elif height <= 0.52:
|
||||
color = COLOR_LOW.lerp(COLOR_MIDDLE, (height - 0.28) / 0.24)
|
||||
elif height <= 0.76:
|
||||
color = COLOR_MIDDLE.lerp(COLOR_HIGH, (height - 0.52) / 0.24)
|
||||
else:
|
||||
color = COLOR_HIGH.lerp(COLOR_PEAK, (height - 0.76) / 0.24)
|
||||
var light_direction := Vector3(-0.35, 0.82, -0.45).normalized()
|
||||
var slope_light := clampf(sample.normal.dot(light_direction), 0.0, 1.0)
|
||||
var shade := lerpf(0.68, 1.0, slope_light)
|
||||
var lit_color := color.lerp(Color.WHITE, sample.intensity * 0.08)
|
||||
return Color(
|
||||
lit_color.r * shade,
|
||||
lit_color.g * shade,
|
||||
lit_color.b * shade,
|
||||
lit_color.a
|
||||
static func _sample_data(scan: SonarScan, sample: SonarSample) -> Color:
|
||||
var height := clampf(
|
||||
inverse_lerp(
|
||||
scan.minimum_elevation_m,
|
||||
scan.maximum_elevation_m,
|
||||
sample.elevation_m
|
||||
),
|
||||
0.0,
|
||||
1.0
|
||||
)
|
||||
var metal := 1.0 if sample.material == SonarMaterial.Value.METAL else 0.0
|
||||
return Color(height, metal, clampf(sample.intensity, 0.0, 1.0))
|
||||
@@ -5,15 +5,32 @@ const SonarScan = preload("res://game/domain/sonar/sonar_scan.gd")
|
||||
const SonarReliefMeshBuilder = preload(
|
||||
"res://game/presentation/sonar/sonar_relief_mesh_builder.gd"
|
||||
)
|
||||
const SonarReliefStyle = preload("res://game/presentation/sonar/sonar_relief_style.gd")
|
||||
|
||||
## Matches the glass of the cockpit sonar monitor (0.690 × 0.434, ratio 1.59).
|
||||
const RENDER_SIZE := Vector2i(960, 604)
|
||||
## Ground width framed by the orthographic camera, independent of the ratio.
|
||||
const VIEW_WIDTH_M := 178.0
|
||||
## Table-like oblique view from behind the vehicle. The aim point sits right
|
||||
## of the scan axis so the relief leaves the right edge to the height scale.
|
||||
const CAMERA_AIM_POINT := Vector3(14.0, -46.0, 50.0)
|
||||
const CAMERA_ELEVATION_DEGREES := 44.0
|
||||
const CAMERA_DISTANCE_M := 120.0
|
||||
const IDLE_PROMPT_SIZE := Vector2(600.0, 64.0)
|
||||
## Height scale drawn at the right edge of the screen, in viewport pixels.
|
||||
const SCALE_BAR_RECT := Rect2(920.0, 70.0, 12.0, 464.0)
|
||||
const SCALE_TICK_COUNT := 3
|
||||
const SCALE_LABEL_SIZE := Vector2(112.0, 40.0)
|
||||
const SCALE_FONT_SIZE := 34
|
||||
|
||||
var _current_mesh_instance: MeshInstance3D
|
||||
var _current_grid_instance: MeshInstance3D
|
||||
var _previous_mesh_instance: MeshInstance3D
|
||||
var _contact_root: Node3D
|
||||
var _idle_prompt: Label
|
||||
var _current_material: StandardMaterial3D
|
||||
var _grid_material: StandardMaterial3D
|
||||
var _previous_material: StandardMaterial3D
|
||||
var _current_material: ShaderMaterial
|
||||
var _previous_material: ShaderMaterial
|
||||
var _depth_scale: TextureRect
|
||||
var _scale_labels: Array[Label] = []
|
||||
var _current_scan_id: StringName = &""
|
||||
var _previous_scan_id: StringName = &""
|
||||
var _built_current_rows := 0
|
||||
@@ -22,9 +39,8 @@ var _built_current_grid_segment_count := 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
size = Vector2i(960, 540)
|
||||
size = RENDER_SIZE
|
||||
render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
positional_shadow_atlas_size = 1024
|
||||
own_world_3d = true
|
||||
_build_render_world()
|
||||
|
||||
@@ -41,11 +57,11 @@ func present(
|
||||
if current_scan.scan_id != _current_scan_id:
|
||||
_current_scan_id = current_scan.scan_id
|
||||
_current_mesh_instance.mesh = ArrayMesh.new()
|
||||
_current_grid_instance.mesh = ArrayMesh.new()
|
||||
_built_current_rows = 0
|
||||
_built_current_triangle_count = 0
|
||||
_built_current_grid_segment_count = 0
|
||||
_rebuild_contacts(current_scan)
|
||||
_update_depth_scale(current_scan)
|
||||
_build_current_rows(current_scan, clampi(visible_rows, 0, current_scan.samples.rows))
|
||||
_update_contact_visibility(current_scan, visible_rows)
|
||||
|
||||
@@ -61,7 +77,7 @@ func present(
|
||||
previous_scan,
|
||||
_previous_material
|
||||
)
|
||||
_previous_material.albedo_color.a = clampf(previous_opacity, 0.0, 1.0)
|
||||
SonarReliefStyle.set_opacity(_previous_material, previous_opacity)
|
||||
|
||||
|
||||
func render_texture() -> Texture2D:
|
||||
@@ -84,6 +100,16 @@ func idle_prompt_visible() -> bool:
|
||||
return _idle_prompt != null and _idle_prompt.visible
|
||||
|
||||
|
||||
## Elevation labels of the height scale, top (highest) to bottom; empty until
|
||||
## a scan has been presented.
|
||||
func depth_scale_texts() -> PackedStringArray:
|
||||
var texts := PackedStringArray()
|
||||
if _depth_scale != null and _depth_scale.visible:
|
||||
for label: Label in _scale_labels:
|
||||
texts.append(label.text)
|
||||
return texts
|
||||
|
||||
|
||||
func visible_contact_count() -> int:
|
||||
var count := 0
|
||||
for child: Node in _contact_root.get_children():
|
||||
@@ -96,24 +122,17 @@ func _build_render_world() -> void:
|
||||
var environment_node := WorldEnvironment.new()
|
||||
var environment := Environment.new()
|
||||
environment.background_mode = Environment.BG_COLOR
|
||||
environment.background_color = Color("020407")
|
||||
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
environment.ambient_light_color = Color(0.18, 0.31, 0.34)
|
||||
environment.ambient_light_energy = 0.42
|
||||
environment.background_color = SonarReliefStyle.COLOR_BACKGROUND
|
||||
environment.ambient_light_source = Environment.AMBIENT_SOURCE_DISABLED
|
||||
environment_node.environment = environment
|
||||
add_child(environment_node)
|
||||
|
||||
_current_material = _relief_material(false)
|
||||
_grid_material = _surface_grid_material()
|
||||
_previous_material = _relief_material(true)
|
||||
_current_material = SonarReliefStyle.relief_material(false)
|
||||
_previous_material = SonarReliefStyle.relief_material(true)
|
||||
_current_mesh_instance = MeshInstance3D.new()
|
||||
_current_mesh_instance.name = "CurrentRelief"
|
||||
_current_mesh_instance.mesh = ArrayMesh.new()
|
||||
add_child(_current_mesh_instance)
|
||||
_current_grid_instance = MeshInstance3D.new()
|
||||
_current_grid_instance.name = "CurrentReliefGrid"
|
||||
_current_grid_instance.mesh = ArrayMesh.new()
|
||||
add_child(_current_grid_instance)
|
||||
_previous_mesh_instance = MeshInstance3D.new()
|
||||
_previous_mesh_instance.name = "PreviousRelief"
|
||||
_previous_mesh_instance.mesh = ArrayMesh.new()
|
||||
@@ -128,72 +147,88 @@ func _build_render_world() -> void:
|
||||
var camera := Camera3D.new()
|
||||
camera.name = "SonarCamera"
|
||||
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
|
||||
camera.size = 116.0
|
||||
camera.keep_aspect = Camera3D.KEEP_WIDTH
|
||||
camera.size = VIEW_WIDTH_M
|
||||
camera.far = 240.0
|
||||
camera.position = Vector3(0.0, 65.0, -30.0)
|
||||
var elevation := deg_to_rad(CAMERA_ELEVATION_DEGREES)
|
||||
camera.position = CAMERA_AIM_POINT + CAMERA_DISTANCE_M * Vector3(
|
||||
0.0, sin(elevation), -cos(elevation)
|
||||
)
|
||||
add_child(camera)
|
||||
camera.look_at(Vector3(0.0, -38.0, 58.0), Vector3.UP)
|
||||
camera.look_at(CAMERA_AIM_POINT, Vector3.UP)
|
||||
camera.current = true
|
||||
|
||||
var key_light := DirectionalLight3D.new()
|
||||
key_light.rotation_degrees = Vector3(-52.0, -28.0, 0.0)
|
||||
key_light.light_energy = 1.25
|
||||
key_light.shadow_enabled = true
|
||||
add_child(key_light)
|
||||
var fill_light := DirectionalLight3D.new()
|
||||
fill_light.rotation_degrees = Vector3(-28.0, 145.0, 18.0)
|
||||
fill_light.light_energy = 0.38
|
||||
add_child(fill_light)
|
||||
_build_depth_scale()
|
||||
_build_idle_prompt()
|
||||
|
||||
|
||||
func _build_idle_prompt() -> void:
|
||||
_idle_prompt = Label.new()
|
||||
_idle_prompt.name = "IdlePrompt"
|
||||
_idle_prompt.position = Vector2(180.0, 238.0)
|
||||
_idle_prompt.size = Vector2(600.0, 64.0)
|
||||
_idle_prompt.position = (Vector2(RENDER_SIZE) - IDLE_PROMPT_SIZE) * 0.5
|
||||
_idle_prompt.size = IDLE_PROMPT_SIZE
|
||||
_idle_prompt.text = "SONAR PRÊT · F POUR ÉMETTRE"
|
||||
_idle_prompt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_idle_prompt.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_idle_prompt.add_theme_font_size_override("font_size", 26)
|
||||
_idle_prompt.add_theme_color_override("font_color", Color(0.38, 0.88, 0.86))
|
||||
_idle_prompt.add_theme_color_override("font_color", SonarReliefStyle.COLOR_MIDDLE)
|
||||
_idle_prompt.add_theme_color_override("font_outline_color", Color(0.0, 0.04, 0.05))
|
||||
_idle_prompt.add_theme_constant_override("outline_size", 6)
|
||||
add_child(_idle_prompt)
|
||||
|
||||
|
||||
func _relief_material(transparent: bool) -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
material.vertex_color_use_as_albedo = true
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
if transparent:
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
material.albedo_color = Color(1.0, 1.0, 1.0, 0.0)
|
||||
return material
|
||||
## Height legend: the relief palette is relative to the scan, so the bar shows
|
||||
## the real elevations of its two ends and of the steps in between.
|
||||
func _build_depth_scale() -> void:
|
||||
var gradient := GradientTexture2D.new()
|
||||
gradient.gradient = SonarReliefStyle.height_gradient()
|
||||
gradient.fill_from = Vector2(0.0, 1.0)
|
||||
gradient.fill_to = Vector2(0.0, 0.0)
|
||||
gradient.width = int(SCALE_BAR_RECT.size.x)
|
||||
gradient.height = int(SCALE_BAR_RECT.size.y)
|
||||
_depth_scale = TextureRect.new()
|
||||
_depth_scale.name = "DepthScale"
|
||||
_depth_scale.texture = gradient
|
||||
_depth_scale.position = SCALE_BAR_RECT.position
|
||||
_depth_scale.size = SCALE_BAR_RECT.size
|
||||
_depth_scale.visible = false
|
||||
add_child(_depth_scale)
|
||||
for tick in SCALE_TICK_COUNT:
|
||||
var label := Label.new()
|
||||
label.size = SCALE_LABEL_SIZE
|
||||
label.position = Vector2(
|
||||
-label.size.x - 8.0,
|
||||
SCALE_BAR_RECT.size.y * tick / float(SCALE_TICK_COUNT - 1) - label.size.y * 0.5
|
||||
)
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", SCALE_FONT_SIZE)
|
||||
label.add_theme_color_override("font_color", SonarReliefStyle.COLOR_MIDDLE)
|
||||
_depth_scale.add_child(label)
|
||||
_scale_labels.append(label)
|
||||
|
||||
|
||||
func _surface_grid_material() -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
material.albedo_color = Color(0.32, 0.95, 0.91, 0.34)
|
||||
material.emission_enabled = true
|
||||
material.emission = Color(0.18, 0.78, 0.76)
|
||||
material.emission_energy_multiplier = 1.35
|
||||
return material
|
||||
func _update_depth_scale(scan: SonarScan) -> void:
|
||||
for tick in _scale_labels.size():
|
||||
var elevation := lerpf(
|
||||
scan.maximum_elevation_m,
|
||||
scan.minimum_elevation_m,
|
||||
tick / float(_scale_labels.size() - 1)
|
||||
)
|
||||
_scale_labels[tick].text = "%.0f m" % elevation
|
||||
_depth_scale.visible = true
|
||||
|
||||
|
||||
func _build_current_rows(scan: SonarScan, visible_rows: int) -> void:
|
||||
var mesh := _current_mesh_instance.mesh as ArrayMesh
|
||||
var grid_mesh := _current_grid_instance.mesh as ArrayMesh
|
||||
while _built_current_rows < visible_rows:
|
||||
if _built_current_rows > 0:
|
||||
_built_current_triangle_count += SonarReliefMeshBuilder.add_row_band(
|
||||
var triangles := SonarReliefMeshBuilder.add_row_band(
|
||||
mesh, scan, _built_current_rows, _current_material
|
||||
)
|
||||
_built_current_grid_segment_count += SonarReliefMeshBuilder.add_grid_band(
|
||||
grid_mesh, scan, _built_current_rows, _grid_material
|
||||
_built_current_triangle_count += triangles
|
||||
_built_current_grid_segment_count += (
|
||||
triangles / 2 * SonarReliefMeshBuilder.GRID_SEGMENTS_PER_CELL
|
||||
)
|
||||
_built_current_rows += 1
|
||||
|
||||
@@ -210,13 +245,9 @@ func _rebuild_contacts(scan: SonarScan) -> void:
|
||||
marker.position = contact.center_m + Vector3.UP * 1.4
|
||||
marker.visible = false
|
||||
marker.set_meta("forward_distance_m", contact.center_m.z)
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.albedo_color = SonarReliefMeshBuilder.COLOR_METAL
|
||||
material.emission_enabled = true
|
||||
material.emission = SonarReliefMeshBuilder.COLOR_METAL
|
||||
material.emission_energy_multiplier = 4.0
|
||||
marker.material_override = material
|
||||
marker.material_override = SonarReliefStyle.marker_material(
|
||||
SonarReliefStyle.COLOR_METAL
|
||||
)
|
||||
_contact_root.add_child(marker)
|
||||
|
||||
|
||||
@@ -244,11 +275,7 @@ func _add_submarine_marker() -> void:
|
||||
box.size = Vector3(3.2, 1.1, 5.8)
|
||||
marker.mesh = box
|
||||
marker.position = Vector3(0.0, 0.0, 1.0)
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.albedo_color = Color.WHITE
|
||||
material.emission_enabled = true
|
||||
material.emission = Color.WHITE
|
||||
material.emission_energy_multiplier = 2.4
|
||||
marker.material_override = material
|
||||
marker.material_override = SonarReliefStyle.marker_material(
|
||||
SonarReliefStyle.COLOR_VEHICLE
|
||||
)
|
||||
add_child(marker)
|
||||
@@ -0,0 +1,157 @@
|
||||
class_name SonarReliefStyle
|
||||
extends RefCounted
|
||||
|
||||
## Single owner of the holographic look of the sonar relief: palette, grid
|
||||
## spacing, line widths and intensities. Display only: nothing here is a
|
||||
## gameplay value.
|
||||
|
||||
## Near-black table the hologram floats on. Black also means "unknown".
|
||||
const COLOR_BACKGROUND := Color("010507")
|
||||
## Height ramp, relative to the scan: dark teal low, cyan mid, pale on peaks.
|
||||
const COLOR_DEEP := Color("06404a")
|
||||
const COLOR_MIDDLE := Color("17b8c4")
|
||||
const COLOR_PEAK := Color("d8fbff")
|
||||
## Metal returns keep one warm accent so an anomaly never reads as terrain.
|
||||
const COLOR_METAL := Color("ff7a2a")
|
||||
const COLOR_VEHICLE := Color("f2ffff")
|
||||
|
||||
## Grid lines follow the sample grid: one minor line every N cells.
|
||||
const GRID_MINOR_CELLS := 2.0
|
||||
const GRID_MAJOR_CELLS := 6.0
|
||||
const GRID_MINOR_WIDTH_PX := 1.2
|
||||
const GRID_MAJOR_WIDTH_PX := 2.2
|
||||
const GRID_MINOR_INTENSITY := 0.30
|
||||
const GRID_MAJOR_INTENSITY := 0.80
|
||||
## Iso-height contours, in fractions of the scan's height span.
|
||||
const CONTOUR_STEP := 0.125
|
||||
const CONTOUR_WIDTH_PX := 1.0
|
||||
const CONTOUR_INTENSITY := 0.20
|
||||
## Translucent surface under the grid and the rim that outlines ridges.
|
||||
const SURFACE_INTENSITY := 0.50
|
||||
const RIM_INTENSITY := 0.35
|
||||
const RIM_POWER := 2.5
|
||||
## How much brighter the peaks' lines are than the deep ones.
|
||||
const PEAK_LINE_BOOST := 0.55
|
||||
|
||||
const _SHADER_CODE := """
|
||||
shader_type spatial;
|
||||
render_mode unshaded, cull_disabled, depth_draw_always;
|
||||
|
||||
uniform vec3 background : source_color;
|
||||
uniform vec3 color_deep : source_color;
|
||||
uniform vec3 color_middle : source_color;
|
||||
uniform vec3 color_peak : source_color;
|
||||
uniform vec3 color_metal : source_color;
|
||||
uniform float grid_minor_cells;
|
||||
uniform float grid_major_cells;
|
||||
uniform float grid_minor_width_px;
|
||||
uniform float grid_major_width_px;
|
||||
uniform float grid_minor_intensity;
|
||||
uniform float grid_major_intensity;
|
||||
uniform float contour_step;
|
||||
uniform float contour_width_px;
|
||||
uniform float contour_intensity;
|
||||
uniform float surface_intensity;
|
||||
uniform float rim_intensity;
|
||||
uniform float rim_power;
|
||||
uniform float peak_line_boost;
|
||||
uniform float opacity = 1.0;
|
||||
|
||||
// Anti-aliased line on every multiple of `spacing`, `width_px` wide on screen.
|
||||
// Fades out when lines would be closer than a few pixels, so it never shimmers.
|
||||
float grid_line(float coordinate, float spacing, float width_px) {
|
||||
float scaled = coordinate / spacing;
|
||||
float pixel = max(fwidth(scaled), 1e-5);
|
||||
float distance_px = abs(fract(scaled - 0.5) - 0.5) / pixel;
|
||||
float line = 1.0 - smoothstep(width_px * 0.5 - 0.5, width_px * 0.5 + 0.5, distance_px);
|
||||
return line * clamp((1.0 / pixel - 2.0) / 3.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
// UV carries (column, row) of the sample grid; COLOR.r the relative height,
|
||||
// COLOR.g the metal flag, COLOR.b the echo intensity.
|
||||
float height = clamp(COLOR.r, 0.0, 1.0);
|
||||
vec3 ramp = mix(color_deep, color_middle, smoothstep(0.0, 0.55, height));
|
||||
ramp = mix(ramp, color_peak, smoothstep(0.55, 1.0, height));
|
||||
// The translucent surface stays in the teal-to-cyan range so peaks do not
|
||||
// wash out to grey; only the lines reach the pale peak colour.
|
||||
vec3 fill = mix(color_deep, color_middle, smoothstep(0.0, 0.85, height));
|
||||
float metal = step(0.5, COLOR.g);
|
||||
ramp = mix(ramp, color_metal, metal);
|
||||
fill = mix(fill, color_metal, metal);
|
||||
|
||||
float minor = max(
|
||||
grid_line(UV.x, grid_minor_cells, grid_minor_width_px),
|
||||
grid_line(UV.y, grid_minor_cells, grid_minor_width_px)
|
||||
);
|
||||
float major = max(
|
||||
grid_line(UV.x, grid_major_cells, grid_major_width_px),
|
||||
grid_line(UV.y, grid_major_cells, grid_major_width_px)
|
||||
);
|
||||
float contour = grid_line(height, contour_step, contour_width_px);
|
||||
float rim = pow(1.0 - abs(dot(normalize(NORMAL), normalize(VIEW))), rim_power);
|
||||
|
||||
float lines = max(minor * grid_minor_intensity, major * grid_major_intensity);
|
||||
lines = max(lines, contour * contour_intensity);
|
||||
lines *= 1.0 + peak_line_boost * height;
|
||||
float surface = surface_intensity * (0.22 + 0.78 * height) * (0.8 + 0.2 * COLOR.b);
|
||||
vec3 glow = fill * (surface + rim * rim_intensity) + ramp * lines;
|
||||
|
||||
ALBEDO = background + glow;
|
||||
//FADE ALPHA = opacity;
|
||||
}
|
||||
"""
|
||||
|
||||
static var _opaque_shader: Shader
|
||||
static var _fading_shader: Shader
|
||||
|
||||
|
||||
## The current scan is opaque: depth-tested, so ridges hide what lies behind
|
||||
## them and nothing depends on draw order. The previous scan fades out with the
|
||||
## same shader, alpha-blended and still writing depth.
|
||||
static func relief_material(fading: bool) -> ShaderMaterial:
|
||||
if _opaque_shader == null:
|
||||
_opaque_shader = Shader.new()
|
||||
_opaque_shader.code = _SHADER_CODE
|
||||
_fading_shader = Shader.new()
|
||||
_fading_shader.code = _SHADER_CODE.replace("//FADE ", "")
|
||||
var material := ShaderMaterial.new()
|
||||
material.shader = _fading_shader if fading else _opaque_shader
|
||||
material.set_shader_parameter("background", COLOR_BACKGROUND)
|
||||
material.set_shader_parameter("color_deep", COLOR_DEEP)
|
||||
material.set_shader_parameter("color_middle", COLOR_MIDDLE)
|
||||
material.set_shader_parameter("color_peak", COLOR_PEAK)
|
||||
material.set_shader_parameter("color_metal", COLOR_METAL)
|
||||
material.set_shader_parameter("grid_minor_cells", GRID_MINOR_CELLS)
|
||||
material.set_shader_parameter("grid_major_cells", GRID_MAJOR_CELLS)
|
||||
material.set_shader_parameter("grid_minor_width_px", GRID_MINOR_WIDTH_PX)
|
||||
material.set_shader_parameter("grid_major_width_px", GRID_MAJOR_WIDTH_PX)
|
||||
material.set_shader_parameter("grid_minor_intensity", GRID_MINOR_INTENSITY)
|
||||
material.set_shader_parameter("grid_major_intensity", GRID_MAJOR_INTENSITY)
|
||||
material.set_shader_parameter("contour_step", CONTOUR_STEP)
|
||||
material.set_shader_parameter("contour_width_px", CONTOUR_WIDTH_PX)
|
||||
material.set_shader_parameter("contour_intensity", CONTOUR_INTENSITY)
|
||||
material.set_shader_parameter("surface_intensity", SURFACE_INTENSITY)
|
||||
material.set_shader_parameter("rim_intensity", RIM_INTENSITY)
|
||||
material.set_shader_parameter("rim_power", RIM_POWER)
|
||||
material.set_shader_parameter("peak_line_boost", PEAK_LINE_BOOST)
|
||||
return material
|
||||
|
||||
|
||||
static func set_opacity(material: ShaderMaterial, opacity: float) -> void:
|
||||
material.set_shader_parameter("opacity", clampf(opacity, 0.0, 1.0))
|
||||
|
||||
|
||||
static func marker_material(color: Color) -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.albedo_color = color
|
||||
return material
|
||||
|
||||
|
||||
## Colour of a relative height (0 low, 1 peak), for the depth scale legend.
|
||||
static func height_gradient() -> Gradient:
|
||||
var gradient := Gradient.new()
|
||||
gradient.offsets = PackedFloat32Array([0.0, 0.55, 1.0])
|
||||
gradient.colors = PackedColorArray([COLOR_DEEP, COLOR_MIDDLE, COLOR_PEAK])
|
||||
return gradient
|
||||
@@ -8,6 +8,14 @@ background_color = Color(0.005, 0.045, 0.065, 1)
|
||||
ambient_light_source = 3
|
||||
ambient_light_color = Color(0.12, 0.32, 0.37, 1)
|
||||
ambient_light_energy = 0.42
|
||||
glow_enabled = true
|
||||
glow_levels/1 = 1.0
|
||||
glow_levels/2 = 1.0
|
||||
glow_levels/3 = 0.6
|
||||
glow_levels/4 = 0.2
|
||||
glow_intensity = 1.0
|
||||
glow_blend_mode = 0
|
||||
glow_hdr_threshold = 1.0
|
||||
fog_enabled = true
|
||||
fog_light_color = Color(0.07, 0.25, 0.3, 1)
|
||||
fog_light_energy = 0.48
|
||||
|
||||
@@ -11,6 +11,17 @@ const CockpitDetailing = preload(
|
||||
const CockpitPressureBulkhead = preload(
|
||||
"res://game/presentation/cockpit/cockpit_pressure_bulkhead.gd"
|
||||
)
|
||||
const CockpitAlertBox = preload("res://game/presentation/cockpit/cockpit_alert_box.gd")
|
||||
const CockpitAnalogGauges = preload(
|
||||
"res://game/presentation/cockpit/cockpit_analog_gauges.gd"
|
||||
)
|
||||
const CockpitSonarConsole = preload(
|
||||
"res://game/presentation/cockpit/cockpit_sonar_console.gd"
|
||||
)
|
||||
const CockpitSwitchPanel = preload(
|
||||
"res://game/presentation/cockpit/cockpit_switch_panel.gd"
|
||||
)
|
||||
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
|
||||
const PauseMenu = preload("res://game/presentation/pause/pause_menu.gd")
|
||||
const ControlsSettingsScreen = preload(
|
||||
"res://game/presentation/settings/controls_settings_screen.gd"
|
||||
@@ -19,7 +30,7 @@ const ProductionInputAdapter = preload(
|
||||
"res://game/infrastructure/input/production_input_adapter.gd"
|
||||
)
|
||||
|
||||
const TEST_COUNT := 78
|
||||
const TEST_COUNT := 91
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
@@ -91,7 +102,17 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
else null
|
||||
)
|
||||
var analog_gauges := (
|
||||
instruments.get_node_or_null("Dashboard/AnalogGauges")
|
||||
instruments.get_node_or_null("Dashboard/AnalogGauges") as CockpitAnalogGauges
|
||||
if instruments != null
|
||||
else null
|
||||
)
|
||||
var alert_box := (
|
||||
instruments.get_node_or_null("AlertBox") as CockpitAlertBox
|
||||
if instruments != null
|
||||
else null
|
||||
)
|
||||
var switch_panel := (
|
||||
instruments.get_node_or_null("SwitchPanel") as CockpitSwitchPanel
|
||||
if instruments != null
|
||||
else null
|
||||
)
|
||||
@@ -106,6 +127,8 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
_expect(detailing != null, "cockpit structural detailing is assembled")
|
||||
_expect(pressure_bulkhead != null, "curved pressure bulkhead is assembled")
|
||||
_expect(analog_gauges != null, "dashboard analog instruments are assembled")
|
||||
_expect(alert_box != null, "alert box is assembled")
|
||||
_expect(switch_panel != null, "switch panel is assembled")
|
||||
_expect(hud != null, "critical HUD overlay is assembled")
|
||||
_expect(sonar_service != null, "production sonar service is assembled")
|
||||
_expect(sonar_display != null, "shared sonar presentation is assembled")
|
||||
@@ -117,17 +140,19 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
or detailing == null
|
||||
or pressure_bulkhead == null
|
||||
or analog_gauges == null
|
||||
or alert_box == null
|
||||
or switch_panel == null
|
||||
or adapter == null
|
||||
or hud == null
|
||||
):
|
||||
return
|
||||
|
||||
_expect(instruments.get_node("%WindowFrame") is MeshInstance3D, "cockpit has a physical porthole frame")
|
||||
_expect(pressure_bulkhead.porthole_frame() is MeshInstance3D, "cockpit has a physical porthole frame")
|
||||
_expect(instruments.get_node("%Dashboard") is Node3D, "cockpit has a physical dashboard")
|
||||
_expect(instruments.get_node("%SonarScreen") is MeshInstance3D, "cockpit reserves a physical sonar display")
|
||||
var sonar_monitor := instruments.get_node("SonarMonitor") as Node3D
|
||||
var sonar_monitor := instruments.get_node("SonarMonitor") as CockpitSonarConsole
|
||||
var sonar_screen := instruments.get_node("%SonarScreen") as MeshInstance3D
|
||||
var sonar_housing := instruments.get_node("SonarMonitor/Housing") as MeshInstance3D
|
||||
var sonar_housing := sonar_monitor.housing()
|
||||
var cockpit_camera := camera.get_node("Camera") as Camera3D
|
||||
var electrical_bank := detailing.get_node("SideEquipment/ElectricalBank") as Node3D
|
||||
_expect(sonar_monitor.position.x > 0.5, "cockpit sonar is mounted on the right side")
|
||||
@@ -147,21 +172,39 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
instruments.find_children("SonarReferenceTrace", "MeshInstance3D", true).is_empty(),
|
||||
"cockpit sonar does not invent a decorative profile"
|
||||
)
|
||||
_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(
|
||||
_shell_surrounds(pressure_bulkhead.hull_shell(), cockpit_camera.global_position),
|
||||
"pressure hull shell surrounds the pilot"
|
||||
)
|
||||
_expect(absf(detailing.battery_needle_degrees()) > 100.0, "analog battery gauge follows the dive snapshot")
|
||||
_expect(detailing.textures_are_loaded(), "cockpit loads its dedicated analog gauge textures")
|
||||
_expect(detailing.gauges_are_dashboard_mounted(), "analog gauges are mounted on the dashboard")
|
||||
_expect(detailing.dashboard_control_count() >= 8, "dashboard exposes physical auxiliary controls")
|
||||
_expect(detailing.side_equipment_count() >= 5, "cockpit sidewalls expose tanks and junction boxes")
|
||||
_expect(detailing.side_control_count() >= 9, "cockpit sidewalls expose valves and pilot controls")
|
||||
_expect(detailing.side_equipment_count() >= 3, "cockpit sidewalls expose a gas bottle and junction boxes")
|
||||
_expect(detailing.side_control_count() >= 3, "cockpit sidewalls expose a valve and pilot controls")
|
||||
_expect(detailing.sonar_control_count() >= 4, "sonar console exposes physical lamps and gain control")
|
||||
_expect(detailing.practical_light_count() == 2, "cockpit uses two caged practical lights")
|
||||
_expect(pressure_bulkhead.panel_count() == 1, "porthole bulkhead uses one continuous curved shell")
|
||||
_expect(pressure_bulkhead.rectangular_whitebox_is_hidden(), "porthole rectangular whitebox is hidden")
|
||||
_expect(analog_gauges.gauge_count() == 4, "dashboard exposes four primary analog instruments")
|
||||
_expect(analog_gauges.indicator_count() == 3, "instrument binnacle exposes three inset pilot lights")
|
||||
_expect(analog_gauges.active_indicator_count() == 1, "exactly one pilot light reflects the alert state")
|
||||
_expect(alert_box.indicator_count() == 3, "alert box exposes three pilot lights")
|
||||
_expect(alert_box.active_indicator_count() == 1, "exactly one pilot light reflects the alert state")
|
||||
_expect(
|
||||
alert_box.active_indicator_name() == "Lamp_Alerte",
|
||||
"normal telemetry lights the green system lamp"
|
||||
)
|
||||
_expect(
|
||||
analog_gauges.pilot_light_count() == 6 and analog_gauges.lit_pilot_light_count() == 0,
|
||||
"gauge panel pilot lights rest dark without per-instrument data"
|
||||
)
|
||||
_expect(
|
||||
switch_panel.control_count() == 10 and switch_panel.controls_are_at_rest(),
|
||||
"decorative switches stay in their rest pose"
|
||||
)
|
||||
_expect(
|
||||
_sonar_texture_matches_glass(instruments.sonar_texture(), sonar_screen),
|
||||
"sonar render target has the aspect ratio of the monitor glass"
|
||||
)
|
||||
_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")
|
||||
@@ -184,6 +227,13 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
_expect(sonar_display.built_row_count() > 0, "renderer reveals near rows incrementally")
|
||||
_expect(sonar_display.built_triangle_count() >= 90, "sonar renders a dense relief surface, not a line")
|
||||
_expect(sonar_display.built_grid_segment_count() > 0, "sonar overlays a perspective grid on the relief")
|
||||
var scale_texts: PackedStringArray = sonar_display.depth_scale_texts()
|
||||
_expect(
|
||||
scale_texts.size() >= 2
|
||||
and scale_texts[0] == "%.0f m" % first_scan.maximum_elevation_m
|
||||
and scale_texts[scale_texts.size() - 1] == "%.0f m" % first_scan.minimum_elevation_m,
|
||||
"sonar height scale shows the real elevation span of the scan"
|
||||
)
|
||||
adapter.sonar_view_toggle_requested.emit()
|
||||
_expect(sonar_display.is_expanded(), "sonar can expand without pausing the dive")
|
||||
adapter.sonar_view_toggle_requested.emit()
|
||||
@@ -294,6 +344,38 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
||||
"fixed dive ticks resume after pause"
|
||||
)
|
||||
|
||||
analog_gauges.present(0.0, 0.5, 1.0, 0.25)
|
||||
_expect(
|
||||
is_equal_approx(analog_gauges.needle_degrees("Needle_Oxygen"), 120.0),
|
||||
"empty gauge needle rests at the lower-left end of the printed scale"
|
||||
)
|
||||
_expect(
|
||||
is_zero_approx(analog_gauges.needle_degrees("Needle_Battery")),
|
||||
"half-scale gauge needle points straight up"
|
||||
)
|
||||
_expect(
|
||||
is_equal_approx(analog_gauges.needle_degrees("Needle_Hull"), -120.0),
|
||||
"full gauge needle reaches the lower-right end clockwise"
|
||||
)
|
||||
_expect(
|
||||
is_equal_approx(analog_gauges.needle_degrees("Needle_Depth"), 60.0),
|
||||
"depth needle uses the same printed sweep"
|
||||
)
|
||||
_expect(
|
||||
_alert_lamp_for(alert_box, DiveAlertLevel.Value.NORMAL) == "Lamp_Alerte",
|
||||
"normal level lights only the system lamp"
|
||||
)
|
||||
_expect(
|
||||
_alert_lamp_for(alert_box, DiveAlertLevel.Value.ATTENTION) == "Lamp_Attention"
|
||||
and _alert_lamp_for(alert_box, DiveAlertLevel.Value.RETURN) == "Lamp_Attention",
|
||||
"attention and return levels light only the attention lamp"
|
||||
)
|
||||
_expect(
|
||||
_alert_lamp_for(alert_box, DiveAlertLevel.Value.CRITICAL) == "Lamp_Danger"
|
||||
and _alert_lamp_for(alert_box, DiveAlertLevel.Value.FAILURE) == "Lamp_Danger",
|
||||
"critical and failure levels light only the danger lamp"
|
||||
)
|
||||
|
||||
|
||||
func _pressed_key(keycode: Key) -> InputEventKey:
|
||||
var event := InputEventKey.new()
|
||||
@@ -327,20 +409,61 @@ func _sonar_screen_is_ahead_of_bank(
|
||||
return true
|
||||
|
||||
|
||||
## The imported housing is an arbitrary mesh, so the clearance is measured on
|
||||
## its triangles: looking straight at the glass, nothing of the housing may sit
|
||||
## level with or in front of the picture.
|
||||
func _sonar_screen_clears_housing(
|
||||
sonar_screen: MeshInstance3D,
|
||||
sonar_housing: MeshInstance3D
|
||||
) -> bool:
|
||||
if sonar_screen == null or sonar_housing == null:
|
||||
if sonar_screen == null or sonar_housing == null or sonar_housing.mesh == null:
|
||||
return false
|
||||
var housing_box := sonar_housing.mesh as BoxMesh
|
||||
if housing_box == null:
|
||||
return false
|
||||
var housing_front_z := (
|
||||
sonar_housing.position.z
|
||||
+ housing_box.size.z * sonar_housing.scale.z * 0.5
|
||||
var glass := sonar_screen.mesh.get_aabb()
|
||||
var screen_to_housing := (
|
||||
sonar_housing.global_transform.affine_inverse() * sonar_screen.global_transform
|
||||
)
|
||||
return sonar_screen.position.z >= housing_front_z + 0.005
|
||||
var outward := (screen_to_housing.basis * Vector3.BACK).normalized()
|
||||
var triangles := sonar_housing.mesh.get_faces()
|
||||
if triangles.is_empty():
|
||||
return false
|
||||
for column in 5:
|
||||
for row in 3:
|
||||
var on_glass := Vector3(
|
||||
lerpf(glass.position.x, glass.end.x, 0.1 + 0.2 * column),
|
||||
lerpf(glass.position.y, glass.end.y, 0.1 + 0.4 * row),
|
||||
glass.position.z - 0.005
|
||||
)
|
||||
var origin := screen_to_housing * on_glass
|
||||
for index in range(0, triangles.size(), 3):
|
||||
if Geometry3D.ray_intersects_triangle(
|
||||
origin,
|
||||
outward,
|
||||
triangles[index],
|
||||
triangles[index + 1],
|
||||
triangles[index + 2]
|
||||
) != null:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _shell_surrounds(shell: MeshInstance3D, point: Vector3) -> bool:
|
||||
if shell == null or shell.mesh == null:
|
||||
return false
|
||||
var local := shell.global_transform.affine_inverse() * point
|
||||
return shell.mesh.get_aabb().grow(-0.05).has_point(local)
|
||||
|
||||
|
||||
func _sonar_texture_matches_glass(texture: Texture2D, sonar_screen: MeshInstance3D) -> bool:
|
||||
if texture == null or sonar_screen == null:
|
||||
return false
|
||||
var glass := sonar_screen.mesh.get_aabb().size
|
||||
var texture_ratio := float(texture.get_width()) / float(texture.get_height())
|
||||
return absf(texture_ratio - glass.x / glass.y) < 0.01
|
||||
|
||||
|
||||
func _alert_lamp_for(alert_box: CockpitAlertBox, level: DiveAlertLevel.Value) -> String:
|
||||
alert_box.present(level)
|
||||
return alert_box.active_indicator_name() if alert_box.active_indicator_count() == 1 else ""
|
||||
|
||||
|
||||
func _binding_for(adapter: ProductionInputAdapter, action_id: StringName) -> String:
|
||||
|
||||