diff --git a/rhythm_coach/assets/career/milestones.json b/rhythm_coach/assets/career/milestones.json index aa25979..e6950f3 100644 --- a/rhythm_coach/assets/career/milestones.json +++ b/rhythm_coach/assets/career/milestones.json @@ -364,6 +364,287 @@ {"time": 54, "text": "Bonne fille. Tu sais aspirer.", "mode": "breath", "duration": 6} ] }, + { + "id": "intro_posture_sitting", + "displayLabel": "Assieds-toi et ouvre-toi", + "level": 4, + "branches": ["obeissance"], + "requires": ["basics"], + "unlocks": ["posture_sitting"], + "sequence": [ + { + "time": 0, + "text": "Aujourd'hui tu obéis à une consigne de position. Assieds-toi bien droite, écarte les genoux, et présente-toi.", + "mode": "breath", + "duration": 8 + }, + { + "time": 8, + "text": "Dans cette position, tu me prends en bouche. Commence doucement.", + "mode": "rhythm", + "from": "tip", + "to": "head", + "bpm": 80, + "duration": 16 + }, + { + "time": 24, + "text": "Reste assise, dos droit. Continue.", + "mode": "lick", + "from": "tip", + "to": "head", + "bpm": 70, + "duration": 16 + }, + { + "time": 40, + "text": "Souffle. Garde la position.", + "mode": "breath", + "duration": 6 + }, + { + "time": 46, + "text": "Encore. Tu restes assise, offerte, et tu bosses.", + "mode": "rhythm", + "from": "head", + "to": "mid", + "bpm": 85, + "duration": 18 + }, + { + "time": 64, + "text": "Bien. Voilà ta position assise. Tu la connais maintenant.", + "mode": "breath", + "duration": 8 + } + ] + }, + { + "id": "intro_posture_standing", + "displayLabel": "Debout, à hauteur", + "level": 5, + "branches": ["obeissance"], + "requires": ["basics"], + "unlocks": ["posture_standing"], + "sequence": [ + { + "time": 0, + "text": "Cette fois tu te mets debout. Jambes écartées, bien stable. Tu vas travailler dans cette position.", + "mode": "breath", + "duration": 8 + }, + { + "time": 8, + "text": "Debout, tu te penches et tu me prends. Vas-y.", + "mode": "rhythm", + "from": "tip", + "to": "head", + "bpm": 85, + "duration": 16 + }, + { + "time": 24, + "text": "Reste debout, ne fléchis pas. Continue.", + "mode": "rhythm", + "from": "head", + "to": "mid", + "bpm": 90, + "duration": 16 + }, + { + "time": 40, + "text": "Souffle. Tu tiens debout.", + "mode": "breath", + "duration": 6 + }, + { + "time": 46, + "text": "Encore. Jambes stables, et tu donnes tout.", + "mode": "lick", + "from": "head", + "to": "mid", + "bpm": 75, + "duration": 18 + }, + { + "time": 64, + "text": "Bien. Position debout acquise.", + "mode": "breath", + "duration": 8 + } + ] + }, + { + "id": "intro_posture_kneeling", + "displayLabel": "À genoux", + "level": 6, + "branches": ["obeissance"], + "requires": ["basics"], + "unlocks": ["posture_kneeling"], + "sequence": [ + { + "time": 0, + "text": "Mets-toi à genoux. C'est ta place. Genoux au sol, dos droit, bouche prête.", + "mode": "breath", + "duration": 8 + }, + { + "time": 8, + "text": "À genoux devant moi, tu me prends. Lève les yeux.", + "mode": "rhythm", + "from": "tip", + "to": "mid", + "bpm": 90, + "duration": 16 + }, + { + "time": 24, + "text": "Reste à genoux. Ne bouge pas de là. Continue.", + "mode": "rhythm", + "from": "head", + "to": "mid", + "bpm": 95, + "duration": 16 + }, + { + "time": 40, + "text": "Souffle. Toujours à genoux.", + "mode": "breath", + "duration": 6 + }, + { + "time": 46, + "text": "Encore. Genoux au sol, tu me sers.", + "mode": "rhythm", + "from": "head", + "to": "mid", + "bpm": 100, + "duration": 18 + }, + { + "time": 64, + "text": "Bien. À genoux, c'est là que tu es le mieux. Tu le sais maintenant.", + "mode": "breath", + "duration": 8 + } + ] + }, + { + "id": "intro_posture_all_fours", + "displayLabel": "À quatre pattes", + "level": 9, + "branches": ["obeissance", "sloppy"], + "requires": ["basics"], + "unlocks": ["posture_all_fours"], + "sequence": [ + { + "time": 0, + "text": "À quatre pattes. Maintenant. Mains et genoux au sol, croupe relevée, tête baissée.", + "mode": "breath", + "duration": 8 + }, + { + "time": 8, + "text": "Dans cette position, tu avances la bouche et tu me prends. Vas-y.", + "mode": "rhythm", + "from": "head", + "to": "mid", + "bpm": 95, + "duration": 16 + }, + { + "time": 24, + "text": "Reste à quatre pattes. Ne te relève pas. Plus profond.", + "mode": "rhythm", + "from": "head", + "to": "throat", + "bpm": 100, + "duration": 16 + }, + { + "time": 40, + "text": "Souffle. Garde la position à quatre pattes.", + "mode": "breath", + "duration": 6 + }, + { + "time": 46, + "text": "Encore. Croupe haute, et tu bosses comme la chienne que tu es.", + "mode": "rhythm", + "from": "head", + "to": "throat", + "bpm": 105, + "duration": 18 + }, + { + "time": 64, + "text": "Bien. À quatre pattes, soumise. Voilà ta position.", + "mode": "breath", + "duration": 8 + } + ] + }, + { + "id": "intro_posture_on_back", + "displayLabel": "Sur le dos, gorge offerte", + "level": 12, + "branches": ["obeissance", "profondeur"], + "requires": ["full_hold"], + "unlocks": ["posture_on_back"], + "requiresCapability": [{ "axis": "hold.full.streak", "min": 5 }], + "sequence": [ + { + "time": 0, + "text": "Allonge-toi sur le dos. Tête en arrière, dans le vide. Ta gorge est alignée — tu vas tout prendre.", + "mode": "breath", + "duration": 8 + }, + { + "time": 8, + "text": "Sur le dos, gorge ouverte, tu me laisses entrer. Doucement.", + "mode": "rhythm", + "from": "mid", + "to": "throat", + "bpm": 90, + "duration": 16 + }, + { + "time": 24, + "text": "Reste sur le dos. Ne relève pas la tête. Plus loin.", + "mode": "hold", + "from": "full", + "duration": 10 + }, + { + "time": 34, + "text": "Tiens. Ta gorge est faite pour ça dans cette position.", + "mode": "hold", + "from": "full", + "duration": 12 + }, + { + "time": 46, + "text": "Souffle. Reste allongée.", + "mode": "breath", + "duration": 8 + }, + { + "time": 54, + "text": "Encore. Sur le dos, gorge grande ouverte, tu me prends jusqu'au fond.", + "mode": "rhythm", + "from": "throat", + "to": "full", + "bpm": 95, + "duration": 16 + }, + { + "time": 70, + "text": "Bien. Sur le dos, tu n'es plus qu'une gorge. Tu connais cette position maintenant.", + "mode": "breath", + "duration": 8 + } + ] + }, { "id": "intro_final_hold_tip", "displayLabel": "Apothéose — sauce sur la langue", diff --git a/rhythm_coach/assets/career/milestones/intro_posture_all_fours_de.json b/rhythm_coach/assets/career/milestones/intro_posture_all_fours_de.json new file mode 100644 index 0000000..7253570 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_all_fours_de.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "Auf alle viere", + "stepTexts": { + "0": "Auf alle viere. Sofort. Hände und Knie auf dem Boden, Hintern hoch, Kopf runter.", + "8": "In dieser Stellung bringst du den Mund nach vorn und nimmst mich. Los.", + "24": "Bleib auf allen vieren. Steh nicht auf. Tiefer.", + "40": "Atme. Bleib auf allen vieren.", + "46": "Nochmal. Hintern hoch, und du arbeitest wie die Schlampe, die du bist.", + "64": "Gut. Auf allen vieren, unterwürfig. Das ist deine Stellung." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_all_fours_en.json b/rhythm_coach/assets/career/milestones/intro_posture_all_fours_en.json new file mode 100644 index 0000000..cf5fc74 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_all_fours_en.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "On all fours", + "stepTexts": { + "0": "On all fours. Now. Hands and knees on the floor, rump up, head down.", + "8": "In that position, you bring your mouth forward and take me. Go on.", + "24": "Stay on all fours. Don't get up. Deeper.", + "40": "Breathe. Keep the all-fours position.", + "46": "Again. Rump high, and you work like the bitch you are.", + "64": "Good. On all fours, submissive. That's your position." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_all_fours_es.json b/rhythm_coach/assets/career/milestones/intro_posture_all_fours_es.json new file mode 100644 index 0000000..7c3372a --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_all_fours_es.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "A cuatro patas", + "stepTexts": { + "0": "A cuatro patas. Ahora. Manos y rodillas en el suelo, culo en alto, cabeza abajo.", + "8": "En esa postura, adelantas la boca y me tomas. Adelante.", + "24": "Quédate a cuatro patas. No te levantes. Más hondo.", + "40": "Respira. Mantén la postura a cuatro patas.", + "46": "Otra vez. Culo en alto, y trabajas como la perra que eres.", + "64": "Bien. A cuatro patas, sumisa. Esa es tu postura." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_kneeling_de.json b/rhythm_coach/assets/career/milestones/intro_posture_kneeling_de.json new file mode 100644 index 0000000..0229ba5 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_kneeling_de.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "Auf die Knie", + "stepTexts": { + "0": "Geh auf die Knie. Das ist dein Platz. Knie auf dem Boden, Rücken gerade, Mund bereit.", + "8": "Auf den Knien vor mir nimmst du mich. Schau hoch.", + "24": "Bleib auf den Knien. Beweg dich nicht weg. Mach weiter.", + "40": "Atme. Immer noch auf den Knien.", + "46": "Nochmal. Knie auf dem Boden, du dienst mir.", + "64": "Gut. Auf den Knien bist du am besten. Jetzt weißt du es." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_kneeling_en.json b/rhythm_coach/assets/career/milestones/intro_posture_kneeling_en.json new file mode 100644 index 0000000..a074492 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_kneeling_en.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "On your knees", + "stepTexts": { + "0": "Get on your knees. That's your place. Knees on the floor, back straight, mouth ready.", + "8": "On your knees in front of me, you take me. Look up.", + "24": "Stay on your knees. Don't move from there. Keep going.", + "40": "Breathe. Still on your knees.", + "46": "Again. Knees on the floor, you serve me.", + "64": "Good. On your knees is where you're best. You know it now." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_kneeling_es.json b/rhythm_coach/assets/career/milestones/intro_posture_kneeling_es.json new file mode 100644 index 0000000..a405743 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_kneeling_es.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "De rodillas", + "stepTexts": { + "0": "Ponte de rodillas. Ese es tu sitio. Rodillas en el suelo, espalda recta, boca lista.", + "8": "De rodillas ante mí, me tomas. Levanta la mirada.", + "24": "Quédate de rodillas. No te muevas de ahí. Continúa.", + "40": "Respira. Sigues de rodillas.", + "46": "Otra vez. Rodillas en el suelo, me sirves.", + "64": "Bien. De rodillas es donde mejor estás. Ya lo sabes." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_on_back_de.json b/rhythm_coach/assets/career/milestones/intro_posture_on_back_de.json new file mode 100644 index 0000000..a5386b9 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_on_back_de.json @@ -0,0 +1,12 @@ +{ + "displayLabel": "Auf dem Rücken, Kehle dargeboten", + "stepTexts": { + "0": "Leg dich auf den Rücken. Kopf nach hinten, über die Kante. Deine Kehle ist ausgerichtet — du nimmst alles.", + "8": "Auf dem Rücken, Kehle offen, lässt du mich rein. Langsam.", + "24": "Bleib auf dem Rücken. Heb den Kopf nicht. Weiter.", + "34": "Halt. Deine Kehle ist in dieser Stellung dafür gemacht.", + "46": "Atme. Bleib liegen.", + "54": "Nochmal. Auf dem Rücken, Kehle weit offen, du nimmst mich bis zum Grund.", + "70": "Gut. Auf dem Rücken bist du nur noch eine Kehle. Jetzt kennst du diese Stellung." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_on_back_en.json b/rhythm_coach/assets/career/milestones/intro_posture_on_back_en.json new file mode 100644 index 0000000..398255b --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_on_back_en.json @@ -0,0 +1,12 @@ +{ + "displayLabel": "On your back, throat offered", + "stepTexts": { + "0": "Lie down on your back. Head tipped back, over the edge. Your throat is lined up — you're going to take it all.", + "8": "On your back, throat open, you let me in. Slowly.", + "24": "Stay on your back. Don't lift your head. Further.", + "34": "Hold. Your throat is made for this in this position.", + "46": "Breathe. Stay lying down.", + "54": "Again. On your back, throat wide open, you take me to the root.", + "70": "Good. On your back, you're nothing but a throat. You know this position now." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_on_back_es.json b/rhythm_coach/assets/career/milestones/intro_posture_on_back_es.json new file mode 100644 index 0000000..de9c0be --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_on_back_es.json @@ -0,0 +1,12 @@ +{ + "displayLabel": "Boca arriba, garganta ofrecida", + "stepTexts": { + "0": "Túmbate boca arriba. La cabeza hacia atrás, fuera del borde. Tu garganta queda alineada — vas a tomarlo todo.", + "8": "Boca arriba, garganta abierta, me dejas entrar. Despacio.", + "24": "Quédate boca arriba. No levantes la cabeza. Más adentro.", + "34": "Aguanta. Tu garganta está hecha para esto en esta postura.", + "46": "Respira. Sigue tumbada.", + "54": "Otra vez. Boca arriba, garganta bien abierta, me tomas hasta el fondo.", + "70": "Bien. Boca arriba, no eres más que una garganta. Ya conoces esta postura." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_sitting_de.json b/rhythm_coach/assets/career/milestones/intro_posture_sitting_de.json new file mode 100644 index 0000000..6c935b7 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_sitting_de.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "Setz dich und öffne dich", + "stepTexts": { + "0": "Heute gehorchst du einer Stellungsanweisung. Setz dich gerade hin, spreiz die Knie und präsentier dich.", + "8": "In dieser Stellung nimmst du mich in den Mund. Fang langsam an.", + "24": "Bleib sitzen, Rücken gerade. Mach weiter.", + "40": "Atme. Bleib in der Stellung.", + "46": "Nochmal. Du bleibst sitzen, dargeboten, und du arbeitest.", + "64": "Gut. Das ist deine sitzende Stellung. Jetzt kennst du sie." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_sitting_en.json b/rhythm_coach/assets/career/milestones/intro_posture_sitting_en.json new file mode 100644 index 0000000..5ac2d17 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_sitting_en.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "Sit down and open up", + "stepTexts": { + "0": "Today you obey a position order. Sit up straight, spread your knees, and present yourself.", + "8": "In that position, you take me in your mouth. Start slow.", + "24": "Stay seated, back straight. Keep going.", + "40": "Breathe. Hold the position.", + "46": "Again. You stay seated, offered, and you work.", + "64": "Good. That's your sitting position. You know it now." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_sitting_es.json b/rhythm_coach/assets/career/milestones/intro_posture_sitting_es.json new file mode 100644 index 0000000..1b5e95f --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_sitting_es.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "Siéntate y ábrete", + "stepTexts": { + "0": "Hoy obedeces una orden de postura. Siéntate bien recta, abre las rodillas y preséntate.", + "8": "En esa postura, me tomas en la boca. Empieza despacio.", + "24": "Sigue sentada, espalda recta. Continúa.", + "40": "Respira. Mantén la postura.", + "46": "Otra vez. Te quedas sentada, ofrecida, y trabajas.", + "64": "Bien. Esa es tu postura sentada. Ya la conoces." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_standing_de.json b/rhythm_coach/assets/career/milestones/intro_posture_standing_de.json new file mode 100644 index 0000000..4e0e2d2 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_standing_de.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "Stehend, auf der richtigen Höhe", + "stepTexts": { + "0": "Diesmal stellst du dich hin. Beine auseinander, schön stabil. In dieser Stellung wirst du arbeiten.", + "8": "Im Stehen beugst du dich vor und nimmst mich. Los.", + "24": "Bleib auf den Beinen, knick nicht ein. Mach weiter.", + "40": "Atme. Du hältst dich aufrecht.", + "46": "Nochmal. Beine stabil, und du gibst alles.", + "64": "Gut. Stehende Stellung gelernt." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_standing_en.json b/rhythm_coach/assets/career/milestones/intro_posture_standing_en.json new file mode 100644 index 0000000..3ac1cfd --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_standing_en.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "Standing, at the right height", + "stepTexts": { + "0": "This time you stand up. Legs apart, nice and stable. You're going to work in this position.", + "8": "Standing, you lean in and take me. Go on.", + "24": "Stay on your feet, don't buckle. Keep going.", + "40": "Breathe. You're holding upright.", + "46": "Again. Legs steady, and you give everything.", + "64": "Good. Standing position learned." + } +} diff --git a/rhythm_coach/assets/career/milestones/intro_posture_standing_es.json b/rhythm_coach/assets/career/milestones/intro_posture_standing_es.json new file mode 100644 index 0000000..34315f8 --- /dev/null +++ b/rhythm_coach/assets/career/milestones/intro_posture_standing_es.json @@ -0,0 +1,11 @@ +{ + "displayLabel": "De pie, a la altura justa", + "stepTexts": { + "0": "Esta vez te pones de pie. Piernas separadas, bien estable. Vas a trabajar en esta postura.", + "8": "De pie, te inclinas y me tomas. Adelante.", + "24": "Quédate de pie, no flaquees. Continúa.", + "40": "Respira. Te mantienes erguida.", + "46": "Otra vez. Piernas firmes, y lo das todo.", + "64": "Bien. Postura de pie aprendida." + } +} diff --git a/rhythm_coach/lib/career/models/posture_unlock.dart b/rhythm_coach/lib/career/models/posture_unlock.dart new file mode 100644 index 0000000..b9632da --- /dev/null +++ b/rhythm_coach/lib/career/models/posture_unlock.dart @@ -0,0 +1,33 @@ +import '../../models/posture.dart'; +import 'unlock_key.dart'; + +/// Pont entre le modèle [Posture] (`lib/models/`, sans dépendance carrière) et +/// l'enum carrière [UnlockKey]. Vit côté `career/` pour ne pas faire dépendre +/// `models/` de `career/` (cf. `Posture.unlockKey`, qui renvoie une `String`). +/// +/// Le `switch` explicite référence chaque `UnlockKey.posture*` littéralement — +/// requis par l'invariant `test/milestone_unlock_invariants_test.dart` (scan +/// textuel `UnlockKey.` dans `/lib`) une fois les milestones +/// `intro_posture_*` ajoutées au catalogue. +extension PostureUnlock on Posture { + /// Clé d'unlock carrière de cette posture, ou `null` pour [Posture.free] + /// (toujours disponible, sans milestone). + UnlockKey? get unlockKeyEnum => switch (this) { + Posture.free => null, + Posture.sitting => UnlockKey.postureSitting, + Posture.standing => UnlockKey.postureStanding, + Posture.kneeling => UnlockKey.postureKneeling, + Posture.allFours => UnlockKey.postureAllFours, + Posture.onBack => UnlockKey.postureOnBack, + }; +} + +/// Postures que l'utilisatrice peut se voir imposer, compte tenu des unlocks +/// acquis. [Posture.free] est toujours incluse (jamais débloquée). L'ordre +/// suit `Posture.values` (déterministe). Sert au tirage de la posture d'intro +/// et des postures de break (cf. spec `specs/scripted_breaks.md`). +List availablePostures(Set unlockedKeys) => [ + Posture.free, + for (final p in Posture.values) + if (p != Posture.free && unlockedKeys.contains(p.unlockKeyEnum)) p, + ]; diff --git a/rhythm_coach/lib/career/models/unlock_key.dart b/rhythm_coach/lib/career/models/unlock_key.dart index 44747ef..da94b58 100644 --- a/rhythm_coach/lib/career/models/unlock_key.dart +++ b/rhythm_coach/lib/career/models/unlock_key.dart @@ -102,7 +102,17 @@ enum UnlockKey { // et `suckleBalls` (zone humil pure, level 10-11, gating anatomy en // plus côté MilestoneService). suckleHead, - suckleBalls; + suckleBalls, + // Postures physiques imposées (mise en scène, issue #77). Chacune est + // débloquée par sa milestone d'introduction dédiée (`intro_posture_*`) et + // consommée par le générateur via `availablePostures(unlockedKeys)` + // (`posture_unlock.dart`) pour tirer la posture d'intro / de break. + // `Posture.free` (« confort, au choix ») n'a pas de clé — toujours dispo. + postureSitting, + postureStanding, + postureKneeling, + postureAllFours, + postureOnBack; String get serialized => switch (this) { UnlockKey.basics => 'basics', @@ -139,6 +149,11 @@ enum UnlockKey { UnlockKey.begBalls => 'beg_balls', UnlockKey.suckleHead => 'suckle_head', UnlockKey.suckleBalls => 'suckle_balls', + UnlockKey.postureSitting => 'posture_sitting', + UnlockKey.postureStanding => 'posture_standing', + UnlockKey.postureKneeling => 'posture_kneeling', + UnlockKey.postureAllFours => 'posture_all_fours', + UnlockKey.postureOnBack => 'posture_on_back', }; static UnlockKey? fromString(String? raw) { diff --git a/rhythm_coach/test/posture_unlock_test.dart b/rhythm_coach/test/posture_unlock_test.dart new file mode 100644 index 0000000..2e934d3 --- /dev/null +++ b/rhythm_coach/test/posture_unlock_test.dart @@ -0,0 +1,58 @@ +import 'package:beat_bitch/career/models/posture_unlock.dart'; +import 'package:beat_bitch/career/models/unlock_key.dart'; +import 'package:beat_bitch/models/posture.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PostureUnlock — Posture ↔ UnlockKey', () { + test('free n\'a pas de clé enum, les autres oui', () { + expect(Posture.free.unlockKeyEnum, isNull); + for (final p in Posture.values.where((p) => p != Posture.free)) { + expect(p.unlockKeyEnum, isNotNull, + reason: '$p doit mapper une UnlockKey'); + } + }); + + test('unlockKeyEnum.serialized cohérent avec Posture.unlockKey (String)', + () { + for (final p in Posture.values) { + expect(p.unlockKeyEnum?.serialized, p.unlockKey, + reason: 'le pont enum doit produire la même clé que le modèle'); + } + }); + }); + + group('availablePostures', () { + test('free seule quand aucun unlock', () { + expect(availablePostures({}), [Posture.free]); + }); + + test('inclut une posture débloquée, free toujours en tête', () { + final result = availablePostures({UnlockKey.postureKneeling}); + expect(result, contains(Posture.kneeling)); + expect(result.first, Posture.free); + expect(result, isNot(contains(Posture.allFours))); + }); + + test('toutes débloquées = toutes les postures', () { + final all = { + for (final p in Posture.values) + if (p.unlockKeyEnum != null) p.unlockKeyEnum!, + }; + expect(availablePostures(all).toSet(), Posture.values.toSet()); + }); + + test('ignore les unlocks non-posture', () { + expect(availablePostures({UnlockKey.lickBalls, UnlockKey.throatHold}), + [Posture.free]); + }); + + test('ordre déterministe = ordre de Posture.values', () { + final result = availablePostures({ + UnlockKey.postureOnBack, + UnlockKey.postureSitting, + }); + expect(result, [Posture.free, Posture.sitting, Posture.onBack]); + }); + }); +}