diff --git a/Docs/Migration/07_Architecture_5.5.md b/Docs/Migration/07_Architecture_5.5.md
index 1e89973..daac3c0 100644
--- a/Docs/Migration/07_Architecture_5.5.md
+++ b/Docs/Migration/07_Architecture_5.5.md
@@ -80,6 +80,7 @@ Structure plate (pas de sous-dossier `Public/`) : `PublicIncludePaths.Add(Module
| Plugin | Cible | Remarque |
|---|---|---|
| `ModdingExtra` | runtime | entrée/sortie des données ; trace d'appel en catégorie `LogModdingExtra` (`Verbose`) |
+| `LPAnimCore` | runtime | lot DC1 : `LPAnimRedirect` (redirection des clips) et `LPCoupleSim` (géométrie du couple), rapatriés de l'addon ; module `LPAnimCore` dans `Plugins/LPAnimCore` |
| `StreetMap` | runtime | porté LWC ; 280 villes vérifiées identiques à 4.27 (L7h) |
| `EnhancedInput` | runtime | système d'entrée actif ; exécute les 34 actions héritées de `DefaultInput.ini` |
| `McpAutomationBridge` | **éditeur uniquement** (`TargetAllowList: Editor`) | pilotage de l'éditeur par serveur MCP ; jamais publié (contient un jeton) |
diff --git a/Docs/Migration/20_Chaine_Animations.md b/Docs/Migration/20_Chaine_Animations.md
index 628caf9..fd1b882 100644
--- a/Docs/Migration/20_Chaine_Animations.md
+++ b/Docs/Migration/20_Chaine_Animations.md
@@ -81,7 +81,7 @@ Rien n'est supprimé : un résultat précédent est déplacé dans `precedent_(Editor)"] -->|Public| LP["LifePlay
(Runtime, module primaire)"]
LPET -->|Public
FLPPose dans LPAnimChecks.h| AM["LPAnimMeasure
(Runtime, nouveau S2)"]
LP -->|Public
LPPairSandbox, LPDirectControl| AM
- LP -.->|"StartupModule : SetUALRoot(QuaterniusUALRoot)"| AM
+ LP -->|Public DC1| AC["LPAnimCore
(plugin du jeu : LPAnimRedirect, LPCoupleSim)"]
+ AM -->|Private DC1| AC
+ SB["LPSandbox
(addon)"] -->|DC1| AM
+ SB -->|DC1| AC
+ SB -.->|"LPSandbox.cpp:17 : SetUALRoot(QuaterniusUALRoot)"| AC
AM -->|Public| ENG["Core, CoreUObject, Engine"]
AM -->|Private| JSON["Json"]
```
@@ -96,8 +100,11 @@ Règles : `LPAnimMeasure` ne dépend ni de `LifePlay` ni de `LifePlayEditorTools
| De | Vers | Type | Motif |
|---|---|---|---|
| `LifePlay` | `LPAnimMeasure` | Public | `LPPairSandbox.h/.cpp`, `LPDirectControlSubsystem.cpp`, `LifePlay.cpp` |
+| `LifePlay` | `LPAnimCore` | Public | DC1 : `LPAnimRedirect`, `LPCoupleSim` (plugin du jeu `Plugins/LPAnimCore`) |
+| `LPAnimMeasure` | `LPAnimCore` | Private | DC1 : la mesure utilise `LPAnimRedirect` |
+| `LPSandbox` | `LPAnimMeasure`, `LPAnimCore` | Public | DC1 : mesure et redirection / géométrie du couple |
| `LifePlayEditorTools` | `LifePlay` | Public | existant |
-| `LifePlayEditorTools` | `LPAnimMeasure` | Public | `LPAnimChecks.h` expose `FLPPose` ; commandlets `LPAnimEval`, `LPAnimCatalog`, `LPAnimTwist` |
+| `LifePlayEditorTools` | `LPAnimMeasure` | Public | `LPAnimChecks.h` expose `FLPPose` ; commandlets `LPAnimEval`, `LPAnimCatalog`, `LPAnimTwist` (jusqu'à DC2) |
| `LPAnimMeasure` | `Core`, `CoreUObject`, `Engine` / `Json` | Public / Privé | animations, squelettes ; lecture de `Tools/anim/profiles/*.json` |
| `LPAnimMeasure` | `LifePlay`, `LifePlayEditorTools` | **aucune** | évite un cycle de modules |
@@ -117,12 +124,12 @@ D:\Game\
- **Liaison** : `LifePlay.uproject` déclare `"AdditionalPluginDirectories": ["../LifePlay-Sandbox/Plugins"]` (chemin relatif au `.uproject`) et active `LPSandbox` dans `Plugins`. UBT et l'éditeur y trouvent `LPSandbox.uplugin` (modules `LPSandbox` et `LPAnimMeasure`) ; les DLL du plugin sont compilées avec le jeu (`Build.bat LifePlayEditor Win64 Development`).
- **Pourquoi pas un submodule** : l'addon est un `uproject` mince qui contient le plugin sous `Plugins/LPSandbox/`. Un submodule monté sur `Plugins/LPSandbox` du jeu donnerait `Plugins/LPSandbox/Plugins/LPSandbox/` (plugin imbriqué, non scanné au bon niveau) ; un submodule `External/LifePlay-Sandbox` dupliquerait l'addon à côté du sibling et rendrait ambigu l'exemplaire chargé. Un clone sibling évite les deux problèmes.
-- **Pin** : le jeu consomme un commit précis de l'addon, consigné à la main dans `S3-INT.md` (aujourd'hui `16905c68070992f5139dab907b41b461fb765957`, `lot/SB4-final`, SB4 ouverture par déclencheur éphémère non mémorisée ; précédents : `3d858527fccfdddd8711c7c7448fa001442e0b8a` SB2-O1 correction de `ShowRoom`, `fece7e9a0e63b3058b5d24d96d669a3c9c0e9add` SB2 bibliothèques de scène et de personnages, caméra du socle, état des modes sorti vers les mods, `82dae58380b76c45f60541fd7cb5a0a409700e10` SB1 registre de mods et barre latérale, `3b21fd180c04f0fa49a1b5c9ef1b517ade1cb362` SB0 conception de la sandbox modulaire, `f09c42e5a2bc613cf269cce76c878461ca41c8b7` S7-T2 uplugin, `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` S5-T2 `LPCoupleSim`, `f8102647ccdebad37e4977ccbf2c51e583823a80`, S4-T3). Le pin est mis à jour (hash dans `S3-INT.md` et README de l'addon) à chaque évolution de l'addon que le jeu doit prendre. Reproduire :
+- **Pin** : le jeu consomme un commit précis de l'addon, consigné à la main dans `S3-INT.md` (aujourd'hui `3e9c32f1f27b1abdaf7731dc90496c27ee061718`, `lot/DC1-animcore`, DC1 `LPAnimRedirect` et `LPCoupleSim` rapatriés dans le plugin `LPAnimCore` du jeu ; précédents : `16905c68070992f5139dab907b41b461fb765957` SB4 ouverture par déclencheur éphémère non mémorisée, `3d858527fccfdddd8711c7c7448fa001442e0b8a` SB2-O1 correction de `ShowRoom`, `fece7e9a0e63b3058b5d24d96d669a3c9c0e9add` SB2 bibliothèques de scène et de personnages, caméra du socle, état des modes sorti vers les mods, `82dae58380b76c45f60541fd7cb5a0a409700e10` SB1 registre de mods et barre latérale, `3b21fd180c04f0fa49a1b5c9ef1b517ade1cb362` SB0 conception de la sandbox modulaire, `f09c42e5a2bc613cf269cce76c878461ca41c8b7` S7-T2 uplugin, `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` S5-T2 `LPCoupleSim`, `f8102647ccdebad37e4977ccbf2c51e583823a80`, S4-T3). Le pin est mis à jour (hash dans `S3-INT.md` et README de l'addon) à chaque évolution de l'addon que le jeu doit prendre. Reproduire :
```powershell
cd D:\Game
git clone https://github.com/edujiro5972/LifePlay-Sandbox.git
-git -C LifePlay-Sandbox checkout 16905c68070992f5139dab907b41b461fb765957
+git -C LifePlay-Sandbox checkout 3e9c32f1f27b1abdaf7731dc90496c27ee061718
# le jeu doit être cloné à côté : D:\Game\LifePlay-master 5.5
```
@@ -179,11 +186,12 @@ Le lot S5 ferme la **dérive de fork** entre la sandbox (`LPSandbox/LPCoupleSim.
- [x] **SB2-O1** : correction du défaut O1 (`etat/sandbox/SB2/SB2-O1-BUILD.md` PASS, contrôle visuel utilisateur 2026-10-07 ; tag `lot-SB2-O1-valide`).
- [x] Reports SB2 : (O1) traité (SB2-O1, `3d858527fccfdddd8711c7c7448fa001442e0b8a` : `ShowRoom` symétrique de `HideRoom`, contrôle visuel utilisateur 2026-10-07 « c'est bon ») ; (E3) sans `lps_lib_scene`, la pièce est construite quand même : non traité, séparation physique abandonnée à la porte G-SB3 (décision utilisateur 2026-10-07).
- [x] **DC0** : cartographie et SPEC du découplage jeu / sandbox (`etat/sandbox/DC0/DC0-SPEC.md`, approuvée par l'utilisateur 2026-10-07) ; plan `24_Plan_Decouplage_Sandbox.md`.
-- [ ] **DC1** : Plugin LPAnimCore dans le jeu, LPAnimMeasure réduit à la mesure (DC1-T1..T5).
+- [x] **DC1** : Plugin LPAnimCore dans le jeu, LPAnimMeasure réduit à la mesure (DC1-T1..T5) (DC1-VAL PASS, contrôle visuel utilisateur 2026-10-07 ; tag addon lot-DC1-valide).
- [ ] **DC2** : Commandlets de mods vers un module éditeur de la sandbox (DC2-T1..T5).
- [ ] **DC3** : Scripts, données et sources de mods vers la sandbox (DC3-T1..T5).
- [ ] **DC4** : Preuve « jeu seul », docs, évaluation (DC4-T1..T4).
-- [ ] Reports DC0 (lot propriétaire **DC1**) : § Rupture du couplage : la mention d'un appel à SetUALRoot par FLifePlayModule::StartupModule() est périmée ; seul l'addon l'appelle (LPSandbox.cpp:17, LPSandboxMeasure.cpp:39) — voir DC0-CART b2.
+- [ ] Intégration des déplacements dans le jeu à améliorer (remarque utilisateur au contrôle visuel DC1, 2026-10-07) ; lot propriétaire : à définir, hors DC.
+- [x] Reports DC0 (lot propriétaire **DC1**, traité : texte corrigé en § Rupture du couplage) : § Rupture du couplage : la mention d'un appel à SetUALRoot par FLifePlayModule::StartupModule() est périmée ; seul l'addon l'appelle (LPSandbox.cpp:17, LPSandboxMeasure.cpp:39) — voir DC0-CART b2.
## Lots (S0 … S8)
| Lot | Objet (une ligne) |
diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md
index 46a9a72..432e5b6 100644
--- a/Docs/Migration/Journal_Modifications.md
+++ b/Docs/Migration/Journal_Modifications.md
@@ -1131,6 +1131,7 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
| CHG-155 | 2026-10-05 | ShowCouple | Un lit est reconnu par son nom. Il ne bloque un déplacement que vers le bas, pour se poser ; le retour horizontal n'est plus arrêté et le contact ne renvoie plus la boîte | après avoir quitté le lit, la boîte ne pouvait plus y revenir | revenir au test `DoesPackageExist` et au filtre du flanc seul |
| CHG-156 | 2026-10-05 | Clavier | `LPKeyboardSubsystem` : bouton « Clavier » ajouté à l'exécution dans `MainMenu.SettingsScrollBox` ; panneau pour changer, ajouter ou retirer une touche (actions et axes clavier). Écrit dans `Saved/Config/Windows`, pas dans `GameSettings.sav` | pouvoir réaffecter les touches, dont Q, sans modifier les Blueprints | retirer `LPKeyboardSubsystem.h/.cpp` et la dépendance `UMG` |
| CHG-158 | 2026-10-05 | Réglages | `LPSettingsSubsystem` : onglets Audio, Video, Lewd, General, Keyboard, Misc au-dessus de `SettingsScrollBox`. Les lignes du Blueprint restent en place et sont montrées selon leur texte ; le reste va dans Misc. Keyboard affiche la liste des touches | la liste unique ne montrait pas le clavier | retirer `LPSettingsSubsystem.h/.cpp` |
+| CHG-234 | 2026-10-07 | DC1 | jeu : `Plugins/LPAnimCore/*` (nouveau plugin : `LPAnimRedirect.*`, `LPCoupleSim.h`, `LPCoupleSimCore.cpp`), `LifePlay.uproject`, `Source/LifePlay/LifePlay.Build.cs`, docs (`21_Sandbox_Addon.md`, `20_Chaine_Animations.md`, `07_Architecture_5.5.md`, `etat/sandbox/S3/S3-INT.md`, `etat/sandbox/DC1/*`) ; addon `D:\Game\LifePlay-Sandbox` : `LPSandbox.uproject`, `LPSandbox.uplugin`, `LPAnimMeasure.Build.cs`, `LPAnimMeasure.h`, `LPSandbox.Build.cs`, suppression de `LPAnimRedirect.*`, `LPCoupleSim.h`, `LPCoupleSimCore.cpp` (commit `3e9c32f1f27b1abdaf7731dc90496c27ee061718`, branche `lot/DC1-animcore`, tag `lot-DC1-valide`) | lot DC1 : `LPAnimRedirect` et `LPCoupleSim` rapatriés dans le plugin `LPAnimCore` du jeu, `LPAnimMeasure` réduit à la mesure ; compilation PASS, DC1-VAL PASS, contrôle visuel utilisateur 2026-10-07 ; report DC0 SetUALRoot traité. Motif : le jeu public doit compiler sans le dépôt privé `LifePlay-Sandbox` (premier pas du découplage) | pin `16905c6` → `3e9c32f` (hash complet `3e9c32f1f27b1abdaf7731dc90496c27ee061718`) | `.\Tools\Restore.ps1 -Lot DC1` (jeu) et `.\Tools\Restore.ps1 -Lot DC1-addon -ProjectRoot D:\Game\LifePlay-Sandbox` ; pin `16905c6` |
| CHG-233 | 2026-10-07 | DC0 | `Docs/Migration/24_Plan_Decouplage_Sandbox.md` (nouveau), `Docs/Migration/etat/sandbox/DC0/DC0-CART.md`, `DC0-SPEC.md`, `DC0-VAL.md` (nouveaux) | lot DC0 : plan du découplage jeu / sandbox (lots DC0 à DC4), cartographie et SPEC approuvées par l'utilisateur le 2026-10-07 (plugin `LPAnimCore` dans le jeu, module éditeur `LPModTools` et tout l'outillage de mods dans l'addon, dépôt Sandbox privé) ; documentation seule, aucun code. Motif : le jeu public doit compiler sans le dépôt privé `LifePlay-Sandbox` (`LifePlay` et `LifePlayEditorTools` dépendent de `LPAnimMeasure`) | aucun pin (documentation seule) | supprimer ces fichiers (documentation seule) ; rien n'est commité dans le dépôt du jeu ni poussé |
| CHG-232 | 2026-10-07 | SB4 | addon `D:\Game\LifePlay-Sandbox` : `Plugins/LPSandbox/Source/LPSandbox/LPPairSandbox.cpp`, `LPSandboxModRegistry.cpp`, `LPSandboxModRegistry.h` (SB4-T0 : ouverture par déclencheur éphémère non mémorisée, `sandbox_mode.txt` du pilote), `README.md` (commits `16905c68070992f5139dab907b41b461fb765957` code + pin, tag `lot-SB4-valide`, branche `lot/SB4-final`, tag d'entrée `lot-SB4-avant` = `30e8c18`) ; jeu : `Docs/Migration/etat/sandbox/SB4/SB4-BUILD.md`, `SB4-VAL.md`, `SB4-EVAL.md`, `Docs/Migration/23_Cycle_De_Vie_Mod.md` (§ 7), `Docs/Migration/21_Sandbox_Addon.md`, `Docs/Migration/etat/sandbox/S3/S3-INT.md` | lot SB4 (dernier lot de la sandbox modulaire) : SB4-T0 correctif (ouverture par déclencheur éphémère, `Open=lps_couple` intact), compilation PASS, validation finale F1 à F4 PASS (démarrage, tournées Quaternius et FreeAnimationsPack identiques à SB2-REF, LPAnimEval identique), doc 23 § 7, évaluation `SB4-EVAL.md` : décision GO pour transposer au jeu par petits lots | pin `3d85852` → `16905c6` (hash complet `16905c68070992f5139dab907b41b461fb765957`) | `.\Tools\Restore.ps1 -Lot SB4` (docs du jeu), `.\Tools\Restore.ps1 -Lot SB4-addon -ProjectRoot D:\Game\LifePlay-Sandbox` et `git -C D:\Game\LifePlay-Sandbox checkout lot-SB4-avant` ; rien n'est commité dans le dépôt du jeu ni poussé |
| CHG-231 | 2026-10-07 | SB2-O1 | addon `D:\Game\LifePlay-Sandbox` : `Plugins/LPSandbox/Source/LPSandbox/LPSandboxScene.cpp` (`ALPSandboxScene::ShowRoom` : `SetHiddenInGame(false)` puis `SetVisibility(true, true)` sur meubles et murs), `README.md` (commits `3d858527fccfdddd8711c7c7448fa001442e0b8a` code + `30e8c18` pin, tag `lot-SB2-O1-valide`, branche `lot/SB2-O1`, tag d'entrée `lot-SB2-O1-avant` = `b4ca206`) ; jeu : `Docs/Migration/etat/sandbox/SB2/SB2-O1-BUILD.md`, `Docs/Migration/etat/sandbox/S3/S3-INT.md`, `Docs/Migration/21_Sandbox_Addon.md` (pin, reports SB2, porte G-SB3) | mini-lot SB2-O1 : correction du défaut O1 (meubles et murs invisibles au retour de Mesure, `ShowRoom` rendu symétrique de `HideRoom`) ; compilation PASS ; tournée freeanims identique à SB2-REF (73/73 fichiers, JSON/CSV/TXT identiques) ; contrôle visuel utilisateur « c'est bon » (meubles et murs visibles en revenant de Mesure vers Couple et Interaction). Porte G-SB3 (décision utilisateur 2026-10-07) : SB3 abandonné (plugins physiques séparés non faits) ; les mods du jeu sont du contenu (.lpmod + pak), leurs dépendances sont vérifiées par le registre (prouvé en SB1/SB2), coût ≈ 1 à 1,5 lot, `LPAnimMeasure` utilisé par le jeu en Shipping ; on passe à SB4 ; E3 non traité | pin `fece7e9` → `3d85852` (hash complet `3d858527fccfdddd8711c7c7448fa001442e0b8a`) | `.\Tools\Restore.ps1 -Lot SB2-O1` (docs du jeu), `.\Tools\Restore.ps1 -Lot SB2-O1-addon -ProjectRoot D:\Game\LifePlay-Sandbox` et `git -C D:\Game\LifePlay-Sandbox checkout lot-SB2-O1-avant` ; rien n'est commité dans le dépôt du jeu ni poussé |
diff --git a/Docs/Migration/etat/sandbox/DC1/DC1-BUILD.md b/Docs/Migration/etat/sandbox/DC1/DC1-BUILD.md
new file mode 100644
index 0000000..6074f0b
--- /dev/null
+++ b/Docs/Migration/etat/sandbox/DC1/DC1-BUILD.md
@@ -0,0 +1,66 @@
+# DC1-BUILD : Compilation LifePlayEditor
+
+## Fiche DC1-T2
+
+| Élément | Valeur |
+|---------|--------|
+| Lot | DC1 (plugin LPAnimCore) |
+| Tâche | DC1-T2 |
+| Agent | A-BUILD |
+| Modèle prévu | haiku |
+| Modèle utilisé | haiku |
+| Date | 2026-10-07 |
+
+## Commande lancée
+
+```
+& 'D:\Game\Launcher\Engine\UE_5.5\Engine\Build\BatchFiles\Build.bat' LifePlayEditor Win64 Development -Project='D:\Game\LifePlay-master 5.5\LifePlay.uproject' -WaitMutex *> 'D:\Game\LifePlay-master 5.5\Docs\Migration\etat\sandbox\DC1\DC1_Build_LifePlayEditor.txt'
+```
+
+## Verdict
+
+**PASS** — Code de sortie : **0**
+
+## Erreurs
+
+Aucune
+
+## Avertissements
+
+- **2 messages UBT** : lignes 46-47, non-blocants, concernant le renommage des niveaux d'avertissement en UBT (DeprecationWarningLevel, MSVCDeprecationWarningLevel).
+- **1 dépendance dépréciée** : ligne 50, McpAutomationBridge dépend de StructUtils (déprécié en 5.5).
+- **Aucun avertissement** concernant LPAnimCore, LPAnimMeasure, LPSandbox ou LifePlay.Build.cs
+
+## Tableau des DLL
+
+| Nom | Date (LastWriteTime) | Taille |
+|-----|----------------------|--------|
+| UnrealEditor-LPAnimCore.dll | 07/10/2026 16:16:52 | 0,23 MB |
+| UnrealEditor-LPAnimMeasure.dll | 07/10/2026 16:16:56 | 0,50 MB |
+| UnrealEditor-LifePlay.dll | 07/10/2026 16:17:14 | 1,16 MB |
+| UnrealEditor-LifePlayEditorTools.dll | 07/10/2026 16:17:21 | 1,06 MB |
+
+Toutes les DLL datent d'aujourd'hui (07/10/2026), témoignant de la compilation réussie.
+
+## Structures compilées
+
+- **Module LPAnimCore** : compilé avec succès, DLL générée et présente.
+- **Module LPAnimMeasure** : compilé avec succès, DLL générée et présente.
+- **Module LifePlay** : compilé avec succès, DLL générée et présente.
+- **Module LifePlayEditorTools** : compilé avec succès, DLL générée et présente.
+- **Module LPSandbox** : compilé avec succès depuis les plugins additionnels (AdditionalPluginDirectories).
+
+## Vérifications
+
+- **Éditeur Unreal** : aucun processus en cours au démarrage ✓
+- **D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\Binaries** : N'existe pas (conforme : le dépôt addon est séparé) ✓
+
+## Journal complet
+
+- Fichier : `D:\Game\LifePlay-master 5.5\Docs\Migration\etat\sandbox\DC1\DC1_Build_LifePlayEditor.txt`
+- Durée totale : 58,66 secondes
+- Actions exécutées : 95/95
+
+## Conclusion
+
+Compilation complétée avec succès. Le plugin LPAnimCore est compilé et prêt pour la suite du pipeline DC1.
diff --git a/Docs/Migration/etat/sandbox/DC1/DC1-DOC.md b/Docs/Migration/etat/sandbox/DC1/DC1-DOC.md
new file mode 100644
index 0000000..44ffd39
--- /dev/null
+++ b/Docs/Migration/etat/sandbox/DC1/DC1-DOC.md
@@ -0,0 +1,28 @@
+# DC1-DOC — Pin et documentation du lot DC1
+
+**Fiche DC1-T4 (A-INT, partie pin) et DC1-T5 (A-DOC) — Modèle prévu sonnet | utilisé sonnet**
+
+- Date : 2026-10-07. Aucune commande git d'écriture, aucun fichier de code. Les fins de ligne (LF) des fichiers sont conservées.
+- Nouveau pin de l'addon : `3e9c32f1f27b1abdaf7731dc90496c27ee061718` (`3e9c32f`, branche `lot/DC1-animcore`, tag `lot-DC1-valide`, local, non poussé). Pin précédent : `16905c68070992f5139dab907b41b461fb765957` (SB4).
+
+## DC1-T4 (partie pin)
+
+| Fichier | Lignes touchées |
+|---|---|
+| `Docs/Migration/etat/sandbox/S3/S3-INT.md` | 11 (commit pin), 13 (ancien pin ajouté en tête des pins précédents), 49 (checkout) |
+| `Docs/Migration/21_Sandbox_Addon.md` | 127 (paragraphe Pin : nouveau hash en tête, SB4 en tête des précédents), 132 (checkout) |
+| `D:\Game\LifePlay-Sandbox\README.md` | 69 (commit pin), 75 (checkout) |
+
+## DC1-T5 (documentation)
+
+| Fichier | Lignes touchées |
+|---|---|
+| `Docs/Migration/Journal_Modifications.md` | 1134 : CHG-234 (7 colonnes), juste au-dessus de CHG-233 |
+| `Docs/Migration/21_Sandbox_Addon.md` | 79 (Rupture du couplage : c'est l'addon `LPSandbox.cpp:17` qui appelle `SetUALRoot`, `LPAnimRedirect` / `LPCoupleSim` dans `Plugins/LPAnimCore`) ; 89 à 93 (graphe mermaid : `LPAnimCore`, `LPSandbox`) ; 103 à 105 et 107 (tableau des dépendances) ; 189 (case DC1 cochée) ; 193 (entrée de registre non cochée « Intégration des déplacements… », lot à définir hors DC) ; 194 (report DC0 SetUALRoot coché) |
+| `Docs/Migration/20_Chaine_Animations.md` | 84 (précision : `LPAnimRedirect` et `LPCoupleSim` dans le plugin `LPAnimCore` du jeu, mesure dans `LPAnimMeasure`) |
+| `Docs/Migration/07_Architecture_5.5.md` | 83 (ligne `LPAnimCore` dans le tableau 2.3 Plugins) |
+| `Docs/Migration/etat/sandbox/DC1/DC1-DOC.md` | nouveau |
+
+## Contrôle
+
+`git diff --stat` (lecture seule) : 07 = 1 ligne, 20 = 1 ligne modifiée, 21 = 22 lignes (15 ajoutées, 7 retirées), Journal = 1 ligne, S3-INT = 3 lignes modifiées, README addon = 2 lignes modifiées. Aucun fichier réécrit en entier.
diff --git a/Docs/Migration/etat/sandbox/DC1/DC1-IMPL.md b/Docs/Migration/etat/sandbox/DC1/DC1-IMPL.md
new file mode 100644
index 0000000..125b4e3
--- /dev/null
+++ b/Docs/Migration/etat/sandbox/DC1/DC1-IMPL.md
@@ -0,0 +1,35 @@
+# DC1-IMPL — Plugin LPAnimCore
+
+- **Fiche** : DC1-T1 (A-IMPL-H) — **Modèle** : M3 opus | opus (session principale) — **Dépend de** : DC0-SPEC § 1
+- **Instantanés** : `D:\Game\LifePlay_Backup_5.5\DC1` (jeu : `LifePlay.uproject`, `LifePlay.Build.cs`, `Plugins\LPAnimCore` absent)
+ et `DC1-addon` (16 fichiers de l'addon). Retour arrière proposé : `.\Tools\Restore.ps1 -Lot DC1` et
+ `.\Tools\Restore.ps1 -Lot DC1-addon -ProjectRoot D:\Game\LifePlay-Sandbox`.
+- **Branche addon** : `lot/DC1-animcore` (depuis `lot/SB4-final` = `4795ea2`).
+
+## Jeu
+
+| Fichier | Changement |
+|---|---|
+| `Plugins/LPAnimCore/LPAnimCore.uplugin` | nouveau : module `LPAnimCore`, Runtime, `PreDefault`, Win64, toutes configurations |
+| `Plugins/LPAnimCore/Source/LPAnimCore/LPAnimCore.Build.cs` | nouveau : `Core`, `CoreUObject`, `Engine` publics, structure plate |
+| `Plugins/LPAnimCore/Source/LPAnimCore/LPAnimCore.cpp` | nouveau : `IMPLEMENT_MODULE(FDefaultModuleImpl, LPAnimCore)` |
+| `…/LPAnimRedirect.h/.cpp`, `LPCoupleSim.h`, `LPCoupleSimCore.cpp` | copiés de l'addon ; seules différences : `LPANIMMEASURE_API` → `LPANIMCORE_API` (6 dans `LPAnimRedirect.h` dont 3 lignes, 18 dans `LPCoupleSim.h`) et le commentaire de `SetUALRoot` (périmé : c'est l'addon qui l'appelle, pas `LifePlay`) ; `LPAnimRedirect.cpp` et `LPCoupleSimCore.cpp` identiques octet pour octet (CRLF conservé) |
+| `LifePlay.uproject` | plugin `LPAnimCore` activé (avant `LPSandbox`, qui reste `Optional`) |
+| `Source/LifePlay/LifePlay.Build.cs` | `LPAnimMeasure` → `LPAnimCore` (public) |
+
+Empreintes SHA-256 (16 premiers caractères) : `LPAnimCore.Build.cs` cabb05d300fb9f33, `LPAnimCore.cpp` 2eabcff54d205a58,
+`LPAnimRedirect.cpp` b0a04db568abfae1, `LPAnimRedirect.h` 89304d2e67da47f1, `LPCoupleSim.h` c560a4a8811a2a87,
+`LPCoupleSimCore.cpp` 7a0a276fa9638a75.
+
+## Addon
+
+| Fichier | Changement |
+|---|---|
+| `Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimRedirect.h/.cpp`, `LPCoupleSim.h`, `LPCoupleSimCore.cpp` | supprimés (`git rm`) |
+| `LPAnimMeasure.Build.cs` | `LPAnimCore` en dépendance privée (`LPPoseOrchestrator.cpp`) |
+| `LPAnimMeasure.h` | commentaire d'en-tête |
+| `LPSandbox/LPSandbox.Build.cs` | `LPAnimCore` en dépendance publique |
+| `LPSandbox.uplugin` | `"Plugins": [ LPAnimCore ]`, description |
+| `LPSandbox.uproject` | `AdditionalPluginDirectories` → `../LifePlay-master 5.5/Plugins/LPAnimCore` |
+
+`LifePlayEditorTools.Build.cs` inchangé (garde `LPAnimMeasure` jusqu'à DC2).
diff --git a/Docs/Migration/etat/sandbox/DC1/DC1-VAL.md b/Docs/Migration/etat/sandbox/DC1/DC1-VAL.md
new file mode 100644
index 0000000..a410535
--- /dev/null
+++ b/Docs/Migration/etat/sandbox/DC1/DC1-VAL.md
@@ -0,0 +1,124 @@
+# DC1-VAL — Validation automatique du lot DC1 (DC1-T3, partie automatique)
+
+**Fiche DC1-T3 — Modèle prévu sonnet | utilisé sonnet**
+
+- Date : 2026-10-07. Build testé : celui de `DC1-BUILD.md` (code 0, non recompilé). Lot DC1 : LPAnimRedirect et LPCoupleSim déplacés dans le plugin du jeu `Plugins\LPAnimCore`.
+- Journaux copiés dans `D:\Game\LifePlay_Backup_5.5\DC1-VAL\` : `F1.log`, `F2_quaternius_sandbox.log`, `F2_quaternius_console.txt`, `F2_freeanims_sandbox.log`, `F2_freeanims_console.txt` (+ `F2_*_result.txt` : code et durée).
+- Procédure : méthode de `SB4-VAL.md` / `SB2-VAL.md` (lanceur `LifePlay_pair_sandbox.bat` lancé depuis PowerShell, attente de `Ouvert :`, copie du journal, arrêt du seul `UnrealEditor` lancé ; tournées par `AnimPipeline.ps1 -Pack -From 5 -To 5`, aucun éditeur avant chaque lancement). Aucun git (hors `status`), aucune modification de source, script ou ini.
+- Incident sans effet : une première tentative freeanims, lancée par erreur dans Windows PowerShell 5.1 (`Start-Process powershell`), a échoué immédiatement : `ECHEC : Impossible de trouver un paramètre correspondant au nom « AsHashtable ».` (cmdlet PowerShell 7 ; erreur de mon lancement, pas du build ; aucun éditeur lancé). Relancée dans pwsh 7 (comme le quaternius) ; seul ce second lancement est retenu.
+
+## Tableau des essais
+
+| Essai | Attendu | Constaté (lignes recopiées) | Verdict |
+|---|---|---|---|
+| F1 démarrage | 7 modules, ordre, aucun refus, `Ouvert : lps_couple`, aucun Error/Ensure/Fatal LPSandbox/LPAnimCore/LPAnimMeasure | `LogLPSandboxMods: Display: Modules : 7 sandbox, 16 jeu ; ordre : lps_system_core, lps_system_animmeasure, lps_lib_scene, lps_lib_actors, lps_couple, lps_interaction, lps_asset_integration` ; les 7 en `pret` ; `LogLPSandboxMods: Display: Ouvert : lps_couple`. Lignes citant LPAnimCore : `LogPluginManager: Mounting Project plugin LPAnimCore` et `LogConfig: Branch 'LPAnimCore' had been unloaded. Reloading on-demand took 0.10ms` (aucune erreur de chargement du module). Aucune ligne de refus de module (le mot « refus » n'apparaît que dans `UTS: Unable to launch the Unreal Trace Server ... Accès refusé`, avertissement d'environnement). Seul `Error` : `LogLifePlay: Error: Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable sur GameInstance_0, table laissee telle quelle` (connu, préexistant, hors LPSandbox). Aucun `Ensure`/`Fatal`. Corps, boîte AgMakeout et poses chargés comme à SB2. | PASS |
+| F2 quaternius : avant | `Open=lps_couple` | `Saved\sandbox_mods.ini:3:Open=lps_couple` | PASS |
+| F2 quaternius : journal | `Ouvert : lps_asset_integration (declencheur, non memorise)` + `TOURNEE fermeture demandee` | `LogLPSandbox: Warning: TOURNEE debut 43 clips, 129 etapes` ; `LogLPSandboxMods: Display: Ouvert : lps_asset_integration (declencheur, non memorise)` ; tournée 43 clips ; fermeture demandée. Code 0, 1139 s, verdict `avertissement`. Seul Error : Coiffures. | PASS |
+| F2 quaternius : après | `Open=lps_couple` | `Saved\sandbox_mods.ini:3:Open=lps_couple` | PASS |
+| F2 freeanims : avant | `Open=lps_couple` | `Saved\sandbox_mods.ini:3:Open=lps_couple` | PASS |
+| F2 freeanims : journal | idem | `LogLPSandbox: Warning: TOURNEE debut 10 clips, 30 etapes` ; `LogLPSandboxMods: Display: Ouvert : lps_asset_integration (declencheur, non memorise)` ; `TOURNEE debut 10 clips, 697 etapes` ; `LogLPSandbox: Warning: TOURNEE fermeture demandee`. Code 0, 458 s, verdict `ok`. Seul Error : Coiffures. | PASS |
+| F2 freeanims : après | `Open=lps_couple` | `Saved\sandbox_mods.ini:3:Open=lps_couple` | PASS |
+| F3 quaternius | fichiers identiques, verdict `avertissement`, 8 mêmes avertissements | voir détail | PASS |
+| F3 freeanims | fichiers de données identiques, verdict `ok` | voir détail (20 images diffèrent, les mêmes qu'à SB2/SB4) | PASS (images : à juger) |
+| F4 état final | ini standard, aucun éditeur, git recopié | voir ci-dessous | PASS |
+
+## Détail F3
+
+Référence : `D:\Game\LifePlay_Backup_5.5\SB2-REF\\` ; nouveau : `Saved\AnimPipeline\\`. Comparaison par SHA-256 de chaque fichier commun.
+
+### quaternius (verdict `avertissement`)
+
+- Liste de fichiers : 44 / 44, identique ; 44 fichiers comparés (critère > 40 : OK).
+- Seuls fichiers dont le SHA-256 diffère : `etapes\5_evaluation.json` (champs `debut`, `fin`, `duree_s` 1136 → 1139), `etat.json` (`fin`, `maj`) et `rapport.html` (seule ligne différente : « Généré le » 07/10/2026 12:10:18 → 16:21:02). Tous les autres `.json`, `.csv`, `.txt` (dont `rapport.json`, `eval_*.csv`, `controles_*.csv`, `retarget_pose_*.txt`) sont identiques : 41 fichiers identiques sur 44.
+- Verdict `5_evaluation.json` : `avertissement` dans la référence et dans le nouveau ; 8 avertissements dans chacun, listes identiques (comparées en JSON).
+- Images : aucune dans ce pack.
+
+### freeanims (verdict `ok`, 0 avertissement)
+
+- Liste de fichiers : 73 / 73, identique ; 73 fichiers comparés (critère > 70 : OK).
+- Mêmes exclusions : `etapes\5_evaluation.json` (`debut`, `fin`, `duree_s` 435 → 458), `etat.json` (`fin`, `maj`) et `rapport.html` (« Généré le » 12:31:18 → 17:08:32 uniquement) diffèrent ; tous les `.json`/`.csv`/`.txt` hors ces fichiers sont identiques.
+- Verdict `ok` des deux côtés, 0 avertissement des deux côtés.
+- Images qui diffèrent : 20 fichiers, exactement les mêmes noms que dans SB4-VAL (rendu non déterministe déjà constaté à SB1/SB2/SB4), octets réf → nouveau :
+
+| Fichier (sous `sandbox\`) | Réf → nouveau |
+|---|---|
+| planches\AS_AirKiss.jpg | 148577 → 148509 |
+| planches\AS_AttackInLevitation2.jpg | 139248 → 139290 |
+| planches\AS_ChillDriving.jpg | 187537 → 187639 |
+| planches\AS_ComboHands.jpg | 159892 → 160037 |
+| planches\AS_EatingOnParty.jpg | 141254 → 141301 |
+| planches\AS_FlyingUp.jpg | 119045 → 118961 |
+| planches\AS_GoForward.jpg | 150812 → 150701 |
+| planches\AS_Rest.jpg | 138425 → 138540 |
+| planches\AS_RightLegKick2.jpg | 88121 → 88080 |
+| planches\AS_Talk.jpg | 144201 → 144319 |
+| vignettes\AS_AirKiss.png | 261470 → 261921 |
+| vignettes\AS_AttackInLevitation2.png | 252870 → 253271 |
+| vignettes\AS_ChillDriving.png | 289192 → 289225 |
+| vignettes\AS_ComboHands.png | 269493 → 269106 |
+| vignettes\AS_EatingOnParty.png | 263747 → 263738 |
+| vignettes\AS_FlyingUp.png | 263615 → 263387 |
+| vignettes\AS_GoForward.png | 257039 → 256829 |
+| vignettes\AS_Rest.png | 242540 → 242649 |
+| vignettes\AS_RightLegKick2.png | 265941 → 265478 |
+| vignettes\AS_Talk.png | 260083 → 260572 |
+
+Écarts de taille inférieurs à 0,2 %. Soit 73 − 3 (fichiers exclus) − 20 (images) = 50 fichiers de données strictement identiques.
+
+## État final (F4)
+
+- `Saved\sandbox_mods.ini` :
+```
+[Mods]
+Order=lps_couple,lps_interaction,lps_asset_integration
+Open=lps_couple
+[Enabled]
+lps_couple=true
+lps_interaction=true
+lps_asset_integration=true
+```
+- Processus `UnrealEditor` : 0.
+- `git -C D:\Game\LifePlay-Sandbox status --short` (changements DC1 non commités, attendu ; code de l'addon retiré vers le jeu) :
+```
+ M LPSandbox.uproject
+ M Plugins/LPSandbox/LPSandbox.uplugin
+ M Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimMeasure.Build.cs
+ M Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimMeasure.h
+D Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimRedirect.cpp
+D Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimRedirect.h
+D Plugins/LPSandbox/Source/LPAnimMeasure/LPCoupleSim.h
+D Plugins/LPSandbox/Source/LPAnimMeasure/LPCoupleSimCore.cpp
+ M Plugins/LPSandbox/Source/LPSandbox/LPSandbox.Build.cs
+```
+
+## Anomalies
+
+- Aucune nouvelle. `Error` Coiffures (LogLifePlay) présent à chaque lancement, préexistant. Avertissement `UTS: Unable to launch the Unreal Trace Server ... Accès refusé` : environnement, sans lien.
+- 20 images freeanims différentes (rendu non déterministe, mêmes fichiers qu'à SB2 et SB4) : à juger visuellement si souhaité.
+- Durées légèrement plus longues que la référence (quaternius 1139 s contre 1136 s ; freeanims 458 s contre 435 s) : sans signification (charge machine), dans l'ordre de grandeur.
+- Tentative freeanims ratée par mon mauvais interpréteur (voir en-tête) : sans conséquence.
+
+## Verdict global de la partie automatique
+
+**PASS** : F1 (7 modules, ordre inchangé, plugin LPAnimCore monté, aucun refus ni Error du module), F2 (ouverture par déclencheur non mémorisée, `Open=lps_couple` intact après les deux tournées), F3 (quaternius `avertissement` avec les 8 mêmes avertissements, 44 fichiers ; freeanims `ok`, 73 fichiers ; tous fichiers de données identiques, seules différences : dates, « Généré le » et 20 images non déterministes) et F4 satisfaits. Le déplacement de LPAnimRedirect et LPCoupleSim n'a rien changé aux résultats des commandlets AP.
+
+## Reste à valider par l'utilisateur
+
+(a) Marche du jeu avec clips convertis (`LPAnimTarget.ini` = game).
+(b) ShowCouple.
+
+## Modèles par fiche
+
+| Fiche | Modèle prévu | Modèle utilisé |
+|---|---|---|
+| DC1-T2 | haiku | haiku |
+| DC1-T3 | sonnet | sonnet |
+
+## Contrôle visuel utilisateur (2026-10-07)
+
+(a) marche du jeu avec clips convertis (`LPAnimTarget.ini` = `game`, jeu lancé depuis l'éditeur compilé DC1) et
+(b) ShowCouple : **confirmés** par l'utilisateur (« oui, je confirme »). `LPAnimTarget.ini` remis à son état d'avant le
+test (`Target=sandbox`). Remarque utilisateur : l'intégration des déplacements dans le jeu reste à améliorer (hors
+périmètre DC, inscrit au registre des reports de `21`).
+
+**Verdict DC1-T3 : PASS.**
diff --git a/Docs/Migration/etat/sandbox/S3/S3-INT.md b/Docs/Migration/etat/sandbox/S3/S3-INT.md
index 056a29c..0706ed4 100644
--- a/Docs/Migration/etat/sandbox/S3/S3-INT.md
+++ b/Docs/Migration/etat/sandbox/S3/S3-INT.md
@@ -8,9 +8,9 @@ Date : 2026-10-06 — Role A-INT
|---|---|
| Repo addon | https://github.com/edujiro5972/LifePlay-Sandbox.git |
| Branche | `main` |
-| **Commit pin** | `16905c68070992f5139dab907b41b461fb765957` (`16905c6`, SB4, **local, non pousse** au 2026-10-07) |
+| **Commit pin** | `3e9c32f1f27b1abdaf7731dc90496c27ee061718` (`3e9c32f`, DC1 : `LPAnimRedirect` et `LPCoupleSim` rapatries dans le plugin `LPAnimCore` du jeu, branche `lot/DC1-animcore`, tag `lot-DC1-valide`, **local, non pousse** au 2026-10-07) |
| Contenu du commit | S5-T2 : `LPCoupleSim` (géométrie du couple partagée jeu/addon) ; inclut S4-T3 (retrait GM Interaction legacy), S4-T2 (découpe `LPPairSandbox.cpp`) et S3-T4 (`StartupModule` → `SetUALRoot`) |
-| Pins précédents | `3d858527fccfdddd8711c7c7448fa001442e0b8a` (SB2-O1), `82dae58380b76c45f60541fd7cb5a0a409700e10` (SB1), `f09c42e5a2bc613cf269cce76c878461ca41c8b7` (S7-T2), `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` (S5-T2), `f8102647ccdebad37e4977ccbf2c51e583823a80` (S4-T3), `4de1f0b6764ed3bc575bb687630d3f070e1be221` (S3-T4) |
+| Pins précédents | `16905c68070992f5139dab907b41b461fb765957` (SB4), `3d858527fccfdddd8711c7c7448fa001442e0b8a` (SB2-O1), `82dae58380b76c45f60541fd7cb5a0a409700e10` (SB1), `f09c42e5a2bc613cf269cce76c878461ca41c8b7` (S7-T2), `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` (S5-T2), `f8102647ccdebad37e4977ccbf2c51e583823a80` (S4-T3), `4de1f0b6764ed3bc575bb687630d3f070e1be221` (S3-T4) |
## Structure
@@ -46,7 +46,7 @@ sur la copie effectivement chargee. Pin manuel a la place (ci-dessous).
```powershell
cd D:\Game
git clone https://github.com/edujiro5972/LifePlay-Sandbox.git
-git -C LifePlay-Sandbox checkout 16905c68070992f5139dab907b41b461fb765957
+git -C LifePlay-Sandbox checkout 3e9c32f1f27b1abdaf7731dc90496c27ee061718
# Le jeu doit etre clone a cote : D:\Game\LifePlay-master 5.5
```
diff --git a/LifePlay.uproject b/LifePlay.uproject
index 16b975b..6cffc0a 100644
--- a/LifePlay.uproject
+++ b/LifePlay.uproject
@@ -19,6 +19,10 @@
}
],
"Plugins": [
+ {
+ "Name": "LPAnimCore",
+ "Enabled": true
+ },
{
"Name": "LPSandbox",
"Enabled": true,
diff --git a/Plugins/LPAnimCore/LPAnimCore.uplugin b/Plugins/LPAnimCore/LPAnimCore.uplugin
new file mode 100644
index 0000000..1f33289
--- /dev/null
+++ b/Plugins/LPAnimCore/LPAnimCore.uplugin
@@ -0,0 +1,21 @@
+{
+ "FileVersion": 3,
+ "Version": 1,
+ "VersionName": "1.0.0",
+ "FriendlyName": "LPAnimCore",
+ "Description": "LifePlay : redirection des clips convertis (LPAnimRedirect) et geometrie commune du couple (LPCoupleSim). Utilise par le jeu (LifePlay) et par l'addon LifePlay-Sandbox. Compile dans toutes les configurations.",
+ "Category": "LifePlay",
+ "CreatedBy": "LifePlay",
+ "CanContainContent": false,
+ "Installed": false,
+ "SupportedTargetPlatforms": [
+ "Win64"
+ ],
+ "Modules": [
+ {
+ "Name": "LPAnimCore",
+ "Type": "Runtime",
+ "LoadingPhase": "PreDefault"
+ }
+ ]
+}
diff --git a/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimCore.Build.cs b/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimCore.Build.cs
new file mode 100644
index 0000000..3fdb197
--- /dev/null
+++ b/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimCore.Build.cs
@@ -0,0 +1,18 @@
+// Module LPAnimCore (lot DC1, Docs/Migration/24_Plan_Decouplage_Sandbox.md) : partie runtime de l'ancien
+// LPAnimMeasure utilisee par le jeu (LPAnimRedirect, LPCoupleSim). Aucune dependance vers LifePlay ni vers
+// l'addon LifePlay-Sandbox ; l'addon (LPSandbox, LPAnimMeasure) depend de ce module, jamais l'inverse.
+
+using UnrealBuildTool;
+
+public class LPAnimCore : ModuleRules
+{
+ public LPAnimCore(ReadOnlyTargetRules Target) : base(Target)
+ {
+ PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
+
+ // Structure plate (comme LPAnimMeasure).
+ PublicIncludePaths.Add(ModuleDirectory);
+
+ PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" });
+ }
+}
diff --git a/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimCore.cpp b/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimCore.cpp
new file mode 100644
index 0000000..e0d6dd0
--- /dev/null
+++ b/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimCore.cpp
@@ -0,0 +1,5 @@
+// Module LPAnimCore (lot DC1) : LPAnimRedirect, LPCoupleSim.
+
+#include "Modules/ModuleManager.h"
+
+IMPLEMENT_MODULE(FDefaultModuleImpl, LPAnimCore)
diff --git a/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimRedirect.cpp b/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimRedirect.cpp
new file mode 100644
index 0000000..12dbfd5
--- /dev/null
+++ b/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimRedirect.cpp
@@ -0,0 +1,62 @@
+#include "LPAnimRedirect.h"
+
+#include "Misc/ConfigCacheIni.h"
+#include "Misc/Paths.h"
+
+namespace LPAnimRedirect
+{
+ static FString IniPath()
+ {
+ return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Config/Windows/LPAnimTarget.ini"));
+ }
+
+ // Defaut identique a FLPSandboxConfig::QuaterniusUALRoot : sans appel a SetUALRoot
+ // (commandlet isole, test), le comportement est inchange.
+ static FString& UALRoot()
+ {
+ static FString Root = TEXT("/Game/Quaternius/UAL");
+ return Root;
+ }
+
+ void SetUALRoot(const FString& Root)
+ {
+ if (!Root.IsEmpty())
+ {
+ UALRoot() = Root;
+ }
+ }
+
+ const FString& GetUALRoot()
+ {
+ return UALRoot();
+ }
+
+ bool IsGameTarget()
+ {
+ FString Target;
+ GConfig->GetString(TEXT("LifePlay.Anim"), TEXT("Target"), Target, IniPath());
+ return Target.Equals(TEXT("game"), ESearchCase::IgnoreCase);
+ }
+
+ FString ClipPath(const FString& AssetName)
+ {
+ return FString::Printf(TEXT("%s/%s.%s"), *GetUALRoot(), *AssetName, *AssetName);
+ }
+
+ static FString& BodyRoot()
+ {
+ static FString Root;
+ return Root;
+ }
+
+ void SetBodyRoot(const FString& Root)
+ {
+ BodyRoot() = Root;
+ }
+
+ FString BodyClipPath(const FString& AssetName)
+ {
+ const FString& Root = BodyRoot().IsEmpty() ? GetUALRoot() : BodyRoot();
+ return FString::Printf(TEXT("%s/%s.%s"), *Root, *AssetName, *AssetName);
+ }
+}
diff --git a/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimRedirect.h b/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimRedirect.h
new file mode 100644
index 0000000..7047537
--- /dev/null
+++ b/Plugins/LPAnimCore/Source/LPAnimCore/LPAnimRedirect.h
@@ -0,0 +1,27 @@
+// Choisit qui joue les clips convertis (Bridget / Michael).
+// Saved/Config/Windows/LPAnimTarget.ini : Target=sandbox ou Target=game.
+// sandbox : seule la sandbox les joue. game : la marche du jeu les joue.
+// Les fichiers restent dans /Game/Quaternius/UAL.
+
+#pragma once
+
+#include "CoreMinimal.h"
+
+namespace LPAnimRedirect
+{
+ LPANIMCORE_API bool IsGameTarget();
+
+ LPANIMCORE_API FString ClipPath(const FString& AssetName);
+
+ // Racine des clips convertis. L'addon LifePlay-Sandbox (module LPSandbox) la fixe au
+ // demarrage depuis Config/LPSandbox.ini (cle Sandbox.Roots/QuaterniusUALRoot) ; sans
+ // l'addon, le defaut interne s'applique (lot DC1 : ce module ne lit aucune config de l'addon).
+ LPANIMCORE_API void SetUALRoot(const FString& Root);
+
+ LPANIMCORE_API const FString& GetUALRoot();
+
+ // Lot AP7 : clips convertis (, Bridget B_, Michael M_) dans un dossier distinct de celui des
+ // clips source (pack UE5 : /Game/AnimPacks/). Vide = meme dossier que les clips source (Quaternius).
+ LPANIMCORE_API void SetBodyRoot(const FString& Root);
+ LPANIMCORE_API FString BodyClipPath(const FString& AssetName);
+}
diff --git a/Plugins/LPAnimCore/Source/LPAnimCore/LPCoupleSim.h b/Plugins/LPAnimCore/Source/LPAnimCore/LPCoupleSim.h
new file mode 100644
index 0000000..edbbfb3
--- /dev/null
+++ b/Plugins/LPAnimCore/Source/LPAnimCore/LPCoupleSim.h
@@ -0,0 +1,215 @@
+// Geometrie commune du couple (lot S5-T2, voir Docs/Migration/etat/sandbox/S5/S5-CART.md).
+// Une seule source de verite pour : la transformation mesh -> acteur, l'echantillonnage des poses,
+// le padding et le recalage de la boite englobante, le placement des corps, et le glissement du couple.
+//
+// Deux appelants, qui partagent la geometrie mais gardent leurs reglages :
+// - addon : ALPPairSandboxStage (LPSandbox/LPCoupleSim.cpp) -> FSweepOptions::Sandbox()
+// - jeu : ULPShowCoupleSubsystem (LifePlay/LPShowCoupleSubsystem.cpp) -> FSweepOptions::Show()
+//
+// Tout ce qui differe d'un cote a l'autre passe par FBoxFitParams ou FSweepOptions : le code
+// de mesure et de boite n'est plus duplique, mais aucune option n'est alignee d'office.
+// Regle du module : aucune dependance vers LifePlay ni vers LPSandbox.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Engine/EngineTypes.h"
+#include "Templates/Function.h"
+
+class AActor;
+class ACharacter;
+class UAnimSequence;
+class UBoxComponent;
+class UCharacterMovementComponent;
+class UPrimitiveComponent;
+class USkeletalMeshComponent;
+class UWorld;
+
+namespace LPCoupleSim
+{
+ // Demi-hauteur de capsule par defaut (Person et sandbox ont la meme valeur nominale).
+ inline constexpr double DefaultCapsuleHalf = 88.0;
+ // Pas d'echantillonnage d'une AnimSequence (0, L/8, ... 7L/8).
+ inline constexpr int32 PoseSamples = 8;
+ // Normale au-dela de laquelle une surface est consideree comme un appui (marche, lit).
+ inline constexpr float WalkableNormalZ = 0.6f;
+ // Penetration en-dessous de laquelle on ne depenetre pas.
+ inline constexpr float MinPenetrationDepth = 1.f;
+ // Tolerance de comparaison de position avant teleportation.
+ inline constexpr float MoveEpsilon = 0.1f;
+ // Rotation du mesh squelettique sous l'acteur (lacet -90).
+ inline constexpr double MeshYawDegrees = -90.0;
+
+ // Parametres de mesure. Les valeurs par defaut sont celles du jeu ; la sandbox surcharge
+ // CapsuleHalf et FloorLocalZ (son sol est a z = +10, donc son plancher local vaut 18 - PlayerSpot.Z).
+ struct FBoxFitParams
+ {
+ double CapsuleHalf = DefaultCapsuleHalf;
+
+ // Voie "animations" : FitCoupleBox (sandbox) et FitBox (jeu).
+ FVector AnimPad = FVector(12.0, 12.0, 6.0);
+ double AnimMaxSize = 800.0;
+
+ // Voie "corps vivants" : FitToBodies (jeu).
+ FVector LivePad = FVector(4.0, 4.0, 4.0);
+ FVector LiveMinExtent = FVector(8.0, 8.0, 8.0);
+ double LiveMaxSize = 400.0;
+ double LiveAnchorRadius = 140.0;
+
+ // Fond de boite, en Z local de l'acteur : la plante des pieds est a -CapsuleHalf.
+ double FloorLocalZ = -DefaultCapsuleHalf;
+ double MinExtentZ = 20.0;
+ };
+
+ struct FBoxFit
+ {
+ FVector Center = FVector::ZeroVector;
+ FVector Extent = FVector::ZeroVector;
+ // false : mesure absente ou hors plafond ; l'appelant garde la boite precedente.
+ bool bValid = false;
+ };
+
+ // --- Niveau pur : aucun UObject, aucun UWorld. Testable sans monde. ---
+
+ // Transformation commune du repere du mesh vers celui de l'acteur.
+ LPANIMCORE_API FTransform MeshToActor(double CapsuleHalf);
+
+ // Espace composant a partir des transformations locales et de la table des parents
+ // (Parents[i] < i, ou INDEX_NONE pour la racine, comme dans un FReferenceSkeleton).
+ LPANIMCORE_API void ComposeComponentSpace(
+ const TArray& Parents, const TArray& Local, TArray& OutComponent);
+
+ // Ajoute la position de chaque os (espace composant) a la boite, via MeshToActor.
+ LPANIMCORE_API void AccumulatePoints(
+ const TArray& Component, const FTransform& ToActor, FBox& InOutBounds);
+
+ // Os ecartes de la mesure des corps vivants (extremites et accessoires : la boite suivrait une
+ // main tendue). La voie "animations" ne les ecarte pas : ecart E6 de la cartographie.
+ LPANIMCORE_API bool IsExcludedLiveBone(const FString& BoneName);
+
+ // Boite brute -> boite finale, voie "animations" : padding, plafond, recalage du fond sur FloorLocalZ.
+ LPANIMCORE_API FBoxFit FitFromAnimBounds(const FBox& Bounds, const FBoxFitParams& Params);
+
+ // Boite brute -> boite finale, voie "corps vivants" : padding, etendue minimale, plafond. Pas de recalage.
+ LPANIMCORE_API FBoxFit FitFromLiveBounds(const FBox& Bounds, const FBoxFitParams& Params);
+
+ // Glissement le long d'une surface. TimeFraction : 1 - Hit.Time pour la part de course restante.
+ // bFlattenZ : la sandbox annule la composante verticale (son Z est verrouille par ailleurs).
+ LPANIMCORE_API FVector SlideWish(
+ const FVector& Wish, const FVector& Normal, float TimeFraction, bool bFlattenZ);
+
+ // --- Niveau lecture : assets et composants, sans effet de bord. ---
+
+ // Demi-hauteur reelle du personnage, avec repli (une capsule non initialisee mesure 0).
+ LPANIMCORE_API double CapsuleHalfOf(const ACharacter* Who, double Fallback = DefaultCapsuleHalf);
+
+ // Echantillonne une AnimSequence et ajoute les os a la boite. Os animes seulement,
+ // GetBoneTransform en donnees compressees (raw = false), rejet des poses NaN.
+ LPANIMCORE_API bool AccumulateAnimBounds(
+ const UAnimSequence* Anim, const FTransform& ToActor, FBox& InOutBounds, int32 Samples = PoseSamples);
+
+ // Ajoute les os affiches d'un mesh squelettique a la boite : ancre pelvis / hip, rayon
+ // LiveAnchorRadius, os exclus. Le mesh doit etre a jour (appel apres le tick d'animation).
+ LPANIMCORE_API void AccumulateLiveBody(
+ const USkeletalMeshComponent* Mesh, const FBoxFitParams& Params, FBox& InOutBounds);
+
+ // --- Niveau effets de bord : boite, corps, balayage. ---
+
+ // Cree la boite si besoin (profil commun : objet Pawn, tout Ignore sauf WorldDynamic = Block,
+ // cachee, hors navigation) et applique l'etat de collision. Retourne la boite a stocker.
+ LPANIMCORE_API UBoxComponent* EnsureBox(
+ ACharacter* Lead, UBoxComponent* Existing, FName Name, bool bEnabled);
+
+ // Etendue, centre et rotation nulle. Sans effet si Fit n'est pas valide.
+ LPANIMCORE_API void ApplyBoxFit(UBoxComponent* Box, const FBoxFit& Fit);
+
+ // Place le mesh squelettique sous l'acteur (plante des pieds a -CapsuleHalf, lacet -90).
+ LPANIMCORE_API void PlaceBodyMesh(ACharacter* Body, double CapsuleHalf);
+
+ // --- Soudure du partenaire ---
+
+ // Etat de mouvement memorise avant gel, pour le rendre a la sortie du show.
+ struct FSavedMove
+ {
+ TWeakObjectPtr Move;
+ TEnumAsByte Mode = MOVE_None;
+ float Gravity = 1.f;
+ bool bTick = true;
+ };
+
+ // Arrete le mouvement, le desactive et coupe son tick. Retourne l'etat d'avant.
+ LPANIMCORE_API FSavedMove SaveAndFreeze(ACharacter* Body);
+ // Rend l'etat memorise, puis vide la memoire.
+ LPANIMCORE_API void RestoreMove(FSavedMove& Saved);
+
+ // Attache le partenaire au porteur et le pose sur son point (relatif nul).
+ LPANIMCORE_API void AttachPartner(ACharacter* Lead, ACharacter* Partner);
+ // Detache en gardant la position monde.
+ LPANIMCORE_API void Unweld(ACharacter* Partner);
+
+ // --- Balayage du couple ---
+
+ // Tout ce qui differe entre la sandbox et le jeu. Partir de Sandbox() ou de Show().
+ struct FSweepOptions
+ {
+ // Nombre de passes de glissement.
+ int32 MaxIterations = 3;
+ // SweepMultiByChannel (jeu : lits et depenetration ont besoin de tous les contacts)
+ // ou SweepSingleByChannel (sandbox).
+ bool bMultiSweep = true;
+ // Annule la composante verticale du glissement (sandbox).
+ bool bFlattenZ = false;
+ // Un contact dont la normale depasse WalkableNormalZ laisse passer le pas entier (sandbox).
+ // Voie simple seulement.
+ bool bAcceptWalkableStep = false;
+ // Sortir des penetrations au lieu de s'arreter net. Voie multiple seulement.
+ bool bDepenetrate = true;
+ // Ignorer tous les ACharacter du monde (jeu : les PNJ ne bloquent pas le couple).
+ bool bIgnoreAllCharacters = true;
+ // Appui franchissable : le couple se pose dessus au lieu d'etre bloque (jeu : les lits).
+ TFunction IsWalkableSupport;
+ // Acteur a ignorer entierement (jeu : la coque de la chambre, nommee Wall / Wall_*).
+ TFunction IsIgnoredShell;
+ // Appele pour chaque acteur ignore par IsIgnoredShell, afin que l'appelant journalise.
+ TFunction OnIgnoredShell;
+
+ // Reglages du jeu : 3 passes, balayage multiple, depenetration, PNJ et coque ignores.
+ static FSweepOptions Show()
+ {
+ FSweepOptions Out;
+ return Out;
+ }
+
+ // Reglages de la sandbox : 2 passes, balayage simple, arret net en penetration,
+ // marches franchissables, glissement a plat, aucun PNJ ignore.
+ static FSweepOptions Sandbox()
+ {
+ FSweepOptions Out;
+ Out.MaxIterations = 2;
+ Out.bMultiSweep = false;
+ Out.bFlattenZ = true;
+ Out.bAcceptWalkableStep = true;
+ Out.bDepenetrate = false;
+ Out.bIgnoreAllCharacters = false;
+ return Out;
+ }
+ };
+
+ struct FSweepResult
+ {
+ // Position d'acteur retenue (From si rien n'a bouge).
+ FVector Location = FVector::ZeroVector;
+ // Dernier bloqueur en penetration, "Acteur/Composant", pour le journal. Vide sinon.
+ FString Blocker;
+ FVector BlockerNormal = FVector::ZeroVector;
+ float BlockerDepth = 0.f;
+ bool bMoved = false;
+ };
+
+ // Resout un deplacement du couple : la boite glisse, From est la position d'acteur de depart
+ // (sandbox : la position courante du porteur ; jeu : la derniere position sure), Wish le
+ // deplacement voulu. La fonction ne deplace rien : l'appelant applique FSweepResult::Location.
+ LPANIMCORE_API FSweepResult ResolveCoupleMove(
+ UWorld* World, const UBoxComponent* Box, const ACharacter* Lead, const ACharacter* Partner,
+ const FVector& From, const FVector& Wish, const FSweepOptions& Options);
+}
diff --git a/Plugins/LPAnimCore/Source/LPAnimCore/LPCoupleSimCore.cpp b/Plugins/LPAnimCore/Source/LPAnimCore/LPCoupleSimCore.cpp
new file mode 100644
index 0000000..cd8aaab
--- /dev/null
+++ b/Plugins/LPAnimCore/Source/LPAnimCore/LPCoupleSimCore.cpp
@@ -0,0 +1,474 @@
+// Geometrie commune du couple (lot S5-T2) - voir LPCoupleSim.h.
+// Nom de fichier distinct de LPSandbox/LPCoupleSim.cpp : deux modules, deux unites de traduction.
+
+#include "LPCoupleSim.h"
+
+#include "Animation/AnimSequence.h"
+#include "Animation/Skeleton.h"
+#include "Components/BoxComponent.h"
+#include "Components/CapsuleComponent.h"
+#include "Components/SkeletalMeshComponent.h"
+#include "Engine/World.h"
+#include "EngineUtils.h"
+#include "GameFramework/Character.h"
+#include "GameFramework/CharacterMovementComponent.h"
+
+namespace LPCoupleSim
+{
+
+FTransform MeshToActor(const double CapsuleHalf)
+{
+ return FTransform(FRotator(0.0, MeshYawDegrees, 0.0), FVector(0.0, 0.0, -CapsuleHalf));
+}
+
+void ComposeComponentSpace(const TArray& Parents, const TArray& Local, TArray& OutComponent)
+{
+ const int32 Num = Local.Num();
+ OutComponent.SetNum(Num, EAllowShrinking::No);
+ for (int32 Bone = 0; Bone < Num; ++Bone)
+ {
+ const int32 Parent = Parents.IsValidIndex(Bone) ? Parents[Bone] : INDEX_NONE;
+ OutComponent[Bone] = Parent == INDEX_NONE ? Local[Bone] : Local[Bone] * OutComponent[Parent];
+ }
+}
+
+void AccumulatePoints(const TArray& Component, const FTransform& ToActor, FBox& InOutBounds)
+{
+ for (const FTransform& Bone : Component)
+ {
+ InOutBounds += ToActor.TransformPosition(Bone.GetLocation());
+ }
+}
+
+bool IsExcludedLiveBone(const FString& BoneName)
+{
+ static const TCHAR* Excluded[] = {
+ TEXT("hand"), TEXT("finger"), TEXT("foot"), TEXT("toe"), TEXT("twist"),
+ TEXT("breast"), TEXT("hair"), TEXT("eye"), TEXT("jaw"), TEXT("weapon"), TEXT("ik")
+ };
+ for (const TCHAR* Part : Excluded)
+ {
+ if (BoneName.Contains(Part, ESearchCase::IgnoreCase))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+FBoxFit FitFromAnimBounds(const FBox& Bounds, const FBoxFitParams& Params)
+{
+ FBoxFit Fit;
+ if (!Bounds.IsValid || Bounds.GetSize().GetMax() > Params.AnimMaxSize)
+ {
+ return Fit;
+ }
+ Fit.Center = Bounds.GetCenter();
+ Fit.Extent = Bounds.GetExtent() + Params.AnimPad;
+ const double Bottom = Fit.Center.Z - Fit.Extent.Z;
+ if (Bottom < Params.FloorLocalZ)
+ {
+ const double Cut = Params.FloorLocalZ - Bottom;
+ Fit.Extent.Z = FMath::Max(Params.MinExtentZ, Fit.Extent.Z - Cut * 0.5);
+ Fit.Center.Z += Cut * 0.5;
+ }
+ Fit.bValid = true;
+ return Fit;
+}
+
+FBoxFit FitFromLiveBounds(const FBox& Bounds, const FBoxFitParams& Params)
+{
+ FBoxFit Fit;
+ if (!Bounds.IsValid || Bounds.GetSize().GetMax() > Params.LiveMaxSize)
+ {
+ return Fit;
+ }
+ Fit.Center = Bounds.GetCenter();
+ Fit.Extent = Bounds.GetExtent() + Params.LivePad;
+ Fit.Extent.X = FMath::Max(Fit.Extent.X, Params.LiveMinExtent.X);
+ Fit.Extent.Y = FMath::Max(Fit.Extent.Y, Params.LiveMinExtent.Y);
+ Fit.Extent.Z = FMath::Max(Fit.Extent.Z, Params.LiveMinExtent.Z);
+ Fit.bValid = true;
+ return Fit;
+}
+
+FVector SlideWish(const FVector& Wish, const FVector& Normal, const float TimeFraction, const bool bFlattenZ)
+{
+ FVector Out = FVector::VectorPlaneProject(Wish * TimeFraction, Normal);
+ if (bFlattenZ)
+ {
+ Out.Z = 0.0;
+ }
+ return Out;
+}
+
+double CapsuleHalfOf(const ACharacter* Who, const double Fallback)
+{
+ const UCapsuleComponent* Cap = Who ? Who->GetCapsuleComponent() : nullptr;
+ return (Cap && Cap->GetUnscaledCapsuleHalfHeight() > 1.f) ? Cap->GetUnscaledCapsuleHalfHeight() : Fallback;
+}
+
+bool AccumulateAnimBounds(const UAnimSequence* Anim, const FTransform& ToActor, FBox& InOutBounds, const int32 Samples)
+{
+ const USkeleton* Skeleton = Anim ? Anim->GetSkeleton() : nullptr;
+ if (!Skeleton || Samples <= 0)
+ {
+ return false;
+ }
+ const FReferenceSkeleton& Ref = Skeleton->GetReferenceSkeleton();
+ const TArray& RefPose = Ref.GetRefBonePose();
+ const int32 Num = RefPose.Num();
+ if (Num <= 0)
+ {
+ return false;
+ }
+ // Seuls les os pilotes par le clip sont lus ; les autres gardent leur repos.
+ TArray Animated;
+ for (const FTrackToSkeletonMap& Track : Anim->GetCompressedTrackToSkeletonMapTable())
+ {
+ if (RefPose.IsValidIndex(Track.BoneTreeIndex))
+ {
+ Animated.Add(Track.BoneTreeIndex);
+ }
+ }
+ TArray Parents;
+ Parents.SetNumUninitialized(Num);
+ for (int32 Bone = 0; Bone < Num; ++Bone)
+ {
+ Parents[Bone] = Ref.GetParentIndex(Bone);
+ }
+ const double Length = FMath::Max(Anim->GetPlayLength(), 0.01);
+ TArray Local;
+ TArray Component;
+ Local.SetNum(Num);
+ Component.SetNum(Num);
+ for (int32 Step = 0; Step < Samples; ++Step)
+ {
+ const double Time = Length * Step / static_cast(Samples);
+ for (int32 Bone = 0; Bone < Num; ++Bone)
+ {
+ Local[Bone] = RefPose[Bone];
+ }
+ for (const int32 Index : Animated)
+ {
+ FTransform Xform = RefPose[Index];
+ Anim->GetBoneTransform(Xform, FSkeletonPoseBoneIndex(Index), Time, false);
+ if (!Xform.ContainsNaN())
+ {
+ Local[Index] = Xform;
+ }
+ }
+ ComposeComponentSpace(Parents, Local, Component);
+ AccumulatePoints(Component, ToActor, InOutBounds);
+ }
+ return true;
+}
+
+void AccumulateLiveBody(const USkeletalMeshComponent* Mesh, const FBoxFitParams& Params, FBox& InOutBounds)
+{
+ if (!Mesh)
+ {
+ return;
+ }
+ const TArray& Bones = Mesh->GetComponentSpaceTransforms();
+ const int32 Num = Bones.Num();
+ if (Num <= 0)
+ {
+ return;
+ }
+ const FTransform ToActor = Mesh->GetRelativeTransform();
+ int32 Pelvis = INDEX_NONE;
+ for (int32 Bone = 0; Bone < Num; ++Bone)
+ {
+ const FName Name = Mesh->GetBoneName(Bone);
+ if (Name == TEXT("pelvis") || Name == TEXT("hip"))
+ {
+ Pelvis = Bone;
+ break;
+ }
+ }
+ FVector Anchor = FVector::ZeroVector;
+ const bool bAnchor = Pelvis != INDEX_NONE && !Bones[Pelvis].ContainsNaN();
+ if (bAnchor)
+ {
+ Anchor = ToActor.TransformPosition(Bones[Pelvis].GetLocation());
+ }
+ const double RadiusSquared = Params.LiveAnchorRadius * Params.LiveAnchorRadius;
+ for (int32 Bone = 0; Bone < Num; ++Bone)
+ {
+ if (Bones[Bone].ContainsNaN() || IsExcludedLiveBone(Mesh->GetBoneName(Bone).ToString()))
+ {
+ continue;
+ }
+ const FVector Point = ToActor.TransformPosition(Bones[Bone].GetLocation());
+ if (bAnchor && FVector::DistSquared(Point, Anchor) > RadiusSquared)
+ {
+ continue;
+ }
+ InOutBounds += Point;
+ }
+}
+
+UBoxComponent* EnsureBox(ACharacter* Lead, UBoxComponent* Existing, const FName Name, const bool bEnabled)
+{
+ if (!Lead)
+ {
+ return Existing;
+ }
+ UBoxComponent* Box = Existing;
+ if (!Box)
+ {
+ Box = NewObject(Lead, Name);
+ Box->SetupAttachment(Lead->GetRootComponent());
+ Box->SetCollisionObjectType(ECC_Pawn);
+ Box->SetCollisionResponseToAllChannels(ECR_Ignore);
+ Box->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Block);
+ Box->SetHiddenInGame(true);
+ Box->SetCanEverAffectNavigation(false);
+ Box->RegisterComponent();
+ }
+ Box->SetCollisionEnabled(bEnabled ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision);
+ return Box;
+}
+
+void ApplyBoxFit(UBoxComponent* Box, const FBoxFit& Fit)
+{
+ if (!Box || !Fit.bValid)
+ {
+ return;
+ }
+ Box->SetBoxExtent(Fit.Extent);
+ Box->SetRelativeLocation(Fit.Center);
+ Box->SetRelativeRotation(FRotator::ZeroRotator);
+}
+
+void PlaceBodyMesh(ACharacter* Body, const double CapsuleHalf)
+{
+ if (USkeletalMeshComponent* Mesh = Body ? Body->GetMesh() : nullptr)
+ {
+ Mesh->SetRelativeLocation(FVector(0.0, 0.0, -CapsuleHalf));
+ Mesh->SetRelativeRotation(FRotator(0.0, MeshYawDegrees, 0.0));
+ }
+}
+
+FSavedMove SaveAndFreeze(ACharacter* Body)
+{
+ FSavedMove Saved;
+ UCharacterMovementComponent* Move = Body ? Body->GetCharacterMovement() : nullptr;
+ if (!Move)
+ {
+ return Saved;
+ }
+ Saved.Move = Move;
+ Saved.Mode = Move->MovementMode;
+ Saved.Gravity = Move->GravityScale;
+ Saved.bTick = Move->IsComponentTickEnabled();
+ Move->StopMovementImmediately();
+ Move->DisableMovement();
+ Move->SetComponentTickEnabled(false);
+ return Saved;
+}
+
+void RestoreMove(FSavedMove& Saved)
+{
+ if (UCharacterMovementComponent* Move = Saved.Move.Get())
+ {
+ Move->SetComponentTickEnabled(Saved.bTick);
+ Move->SetMovementMode(Saved.Mode);
+ Move->GravityScale = Saved.Gravity;
+ }
+ Saved = FSavedMove();
+}
+
+void AttachPartner(ACharacter* Lead, ACharacter* Partner)
+{
+ if (!Lead || !Partner)
+ {
+ return;
+ }
+ if (Partner->GetAttachParentActor() != Lead)
+ {
+ Partner->AttachToActor(Lead, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
+ }
+ Partner->SetActorRelativeLocation(FVector::ZeroVector);
+ Partner->SetActorRelativeRotation(FRotator::ZeroRotator);
+}
+
+void Unweld(ACharacter* Partner)
+{
+ if (Partner)
+ {
+ Partner->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
+ }
+}
+
+namespace
+{
+ // Voie simple (sandbox) : un contact a la fois, arret net en penetration, marches franchissables.
+ void ResolveSingle(UWorld& World, const FCollisionShape& Shape, const FQuat& Rot, const FVector& BoxOffset,
+ const FCollisionQueryParams& Params, const FSweepOptions& Options, const FVector& Wish, FVector& InOutBase)
+ {
+ FVector Slide = Wish;
+ for (int32 Step = 0; Step < Options.MaxIterations && !Slide.IsNearlyZero(); ++Step)
+ {
+ const FVector Start = InOutBase + BoxOffset;
+ FHitResult Hit;
+ const bool bHit = World.SweepSingleByChannel(Hit, Start, Start + Slide, Rot, ECC_Pawn, Shape, Params);
+ if (bHit && Options.bAcceptWalkableStep && Hit.Normal.Z > WalkableNormalZ)
+ {
+ InOutBase += Slide;
+ return;
+ }
+ const FVector StepDelta = Slide * (bHit ? Hit.Time : 1.f);
+ if (!StepDelta.IsNearlyZero() && !Hit.bStartPenetrating)
+ {
+ InOutBase += StepDelta;
+ }
+ if (!bHit || Hit.bStartPenetrating)
+ {
+ return;
+ }
+ Slide = SlideWish(Slide, Hit.Normal, 1.f - Hit.Time, Options.bFlattenZ);
+ }
+ }
+
+ // Voie multiple (jeu) : tous les contacts, depenetration, appuis franchissables.
+ void ResolveMulti(UWorld& World, const FCollisionShape& Shape, const FQuat& Rot, const FVector& BoxOffset,
+ const FCollisionQueryParams& Params, const FSweepOptions& Options, const FVector& Wish,
+ FVector& InOutBase, FSweepResult& InOutResult)
+ {
+ FVector Slide = Wish;
+ for (int32 Step = 0; Step < Options.MaxIterations; ++Step)
+ {
+ const FVector Start = InOutBase + BoxOffset;
+ TArray Hits;
+ World.SweepMultiByChannel(Hits, Start, Start + Slide, Rot, ECC_Pawn, Shape, Params);
+ const FHitResult* Block = nullptr;
+ const FHitResult* Deepest = nullptr;
+ FVector Exit = FVector::ZeroVector;
+ bool bStopNet = false;
+ for (const FHitResult& Hit : Hits)
+ {
+ if (Options.IsWalkableSupport && Options.IsWalkableSupport(Hit.GetComponent()))
+ {
+ // Descente sur l'appui : on s'y pose. Deja dedans : on annule la descente et on le traverse.
+ if (!Hit.bStartPenetrating && Hit.Normal.Z > WalkableNormalZ && Slide.Z < -1.f)
+ {
+ Block = &Hit;
+ break;
+ }
+ if (Hit.bStartPenetrating && Hit.Normal.Z > WalkableNormalZ && Slide.Z < 0.f)
+ {
+ Slide.Z = 0.0;
+ }
+ continue;
+ }
+ if (Hit.bStartPenetrating)
+ {
+ if (!Options.bDepenetrate)
+ {
+ bStopNet = true;
+ break;
+ }
+ if (Hit.PenetrationDepth < MinPenetrationDepth)
+ {
+ continue;
+ }
+ if (!Deepest || Hit.PenetrationDepth > Deepest->PenetrationDepth)
+ {
+ Deepest = &Hit;
+ Exit = Hit.Normal * Hit.PenetrationDepth;
+ }
+ continue;
+ }
+ Block = &Hit;
+ break;
+ }
+ if (bStopNet)
+ {
+ return;
+ }
+ // Mur vertical traverse et course qui pointe dedans : le glissement longe le mur.
+ if (Deepest && FMath::Abs(Deepest->Normal.Z) <= WalkableNormalZ && (Slide | Deepest->Normal) < 0.f)
+ {
+ InOutResult.Blocker = FString::Printf(TEXT("%s/%s"),
+ *GetNameSafe(Deepest->GetActor()), *GetNameSafe(Deepest->GetComponent()));
+ InOutResult.BlockerNormal = Deepest->Normal;
+ InOutResult.BlockerDepth = Deepest->PenetrationDepth;
+ Slide = SlideWish(Slide, Deepest->Normal, 1.f, Options.bFlattenZ);
+ }
+ if (!Exit.IsNearlyZero())
+ {
+ InOutBase += Exit;
+ continue;
+ }
+ if (Slide.IsNearlyZero())
+ {
+ return;
+ }
+ if (!Block)
+ {
+ InOutBase += Slide;
+ return;
+ }
+ InOutBase += Slide * Block->Time;
+ Slide = SlideWish(Slide, Block->Normal, 1.f - Block->Time, Options.bFlattenZ);
+ }
+ }
+}
+
+FSweepResult ResolveCoupleMove(UWorld* World, const UBoxComponent* Box, const ACharacter* Lead,
+ const ACharacter* Partner, const FVector& From, const FVector& Wish, const FSweepOptions& Options)
+{
+ FSweepResult Result;
+ Result.Location = From;
+ if (!World || !Box || !Lead || Wish.IsNearlyZero())
+ {
+ return Result;
+ }
+ FCollisionQueryParams Params(SCENE_QUERY_STAT(LPCoupleSim), false, Lead);
+ if (Partner)
+ {
+ Params.AddIgnoredActor(Partner);
+ }
+ if (Options.bIgnoreAllCharacters)
+ {
+ for (TActorIterator It(World); It; ++It)
+ {
+ Params.AddIgnoredActor(*It);
+ }
+ }
+ if (Options.IsIgnoredShell)
+ {
+ for (TActorIterator It(World); It; ++It)
+ {
+ if (!Options.IsIgnoredShell(*It))
+ {
+ continue;
+ }
+ Params.AddIgnoredActor(*It);
+ if (Options.OnIgnoredShell)
+ {
+ Options.OnIgnoredShell(*It);
+ }
+ }
+ }
+ // La boite suit le porteur : son decalage monde est fixe pendant la resolution (la rotation ne change pas).
+ const FVector BoxOffset = Box->GetComponentLocation() - Lead->GetActorLocation();
+ const FCollisionShape Shape = FCollisionShape::MakeBox(Box->GetScaledBoxExtent());
+ const FQuat Rot = Box->GetComponentQuat();
+ FVector Base = From;
+ if (Options.bMultiSweep)
+ {
+ ResolveMulti(*World, Shape, Rot, BoxOffset, Params, Options, Wish, Base, Result);
+ }
+ else
+ {
+ ResolveSingle(*World, Shape, Rot, BoxOffset, Params, Options, Wish, Base);
+ }
+ Result.Location = Base;
+ Result.bMoved = !Base.Equals(From, MoveEpsilon);
+ return Result;
+}
+
+}
diff --git a/Source/LifePlay/LifePlay.Build.cs b/Source/LifePlay/LifePlay.Build.cs
index 93247e9..64a524d 100644
--- a/Source/LifePlay/LifePlay.Build.cs
+++ b/Source/LifePlay/LifePlay.Build.cs
@@ -40,9 +40,9 @@ public class LifePlay : ModuleRules
// Releve de collision (-LPCollisionLog) : nombre de triangles des trimesh Chaos.
PrivateDependencyModuleNames.Add("Chaos");
- // Lot S2 / S3 : LPAnimMeasure (plugin LPSandbox) — LPDirectControlSubsystem et
- // LifePlayEditorTools incluent LPAnimRedirect / LPPoseMeasure.
- PublicDependencyModuleNames.Add("LPAnimMeasure");
+ // Lot DC1 : LPAnimCore (plugin du jeu, Plugins/LPAnimCore) — LPDirectControlSubsystem inclut
+ // LPAnimRedirect, LPShowCoupleSubsystem.h inclut LPCoupleSim (dependance publique).
+ PublicDependencyModuleNames.Add("LPAnimCore");
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });