Aller au contenu principal

Plans (shot) — l'unité produite

Fiche d'entité. Respecte le contrat de README.md : DefState, argent en _eur entiers, temps en Tick (½ journée), stats 0–100, probabilités _p, Rated pour tout ce qui peut mentir, événements domaine.sujet.verbe_au_passé.


1. Rôle dans le jeu

Le plan est le seul objet que le joueur produit. Tout le reste est acheté, écrit, loué, embauché ou subi :

entitéprovenancestatut
Personengagée (contrat, cachet)achetée
Element (décor, caméra, véhicule)louée / construiteachetée
Sceneécrite par l'auteur, dans le pack de contenudonnée
Dayplanifiée par le joueurorganisée
Eventsurvientsubi
Shotrésolu par le moteurproduit

C'est le point de convergence du pipeline du §7 du README : la scène dit ce qu'il faut, les gens et le matériel avec quoi, le plan de travail quand et combien de temps, les événements ce qui déraille — et le plan est la seule chose qui en sort.

Le joueur ne fabrique pas de plans à la main. Il ne choisit ni la valeur de plan, ni le mouvement, ni le nombre de prises. Il crée les conditions : qui est là, dans quel état, à quelle heure, avec quel matériel, sous quelle pression. Le plan est la mesure de ces conditions.

Règle de design à tenir partout : aucune UI ne permet de « régler » la qualité d'un plan. Pour un meilleur plan, il faut remonter la chaîne — payer un meilleur chef op, décaler au matin, refuser trois plans pour en soigner un.


2. Découpage technique : qui décide des plans ?

Le découpage appartient au réalisateur. Le directeur de production ne décide jamais quels plans on fait ; il décide combien on peut s'en payer. Le modèle doit encoder cette asymétrie, sinon il ment sur le métier.

optiondescriptionpourcontre
(a) liste figée de ShotDef dans la SceneDefl'auteur écrit les 14 plansautorable, testable, rejouablepas de négociation : le découpage est un décor
(b) proposé en jeu par le réalisateur, le joueur négocie le nombrel'auteur écrit un découpage de référence hiérarchisé, le réalisateur en tire une proposition gonflée par son ambitionla négociation devient la boucle de jeuplus dur à équilibrer
(c) généré depuis une couverture viséele moteur synthétise « scène dialoguée à 2, 90 s »zéro travail d'auteurplans génériques, aucune intention, invérifiable en test

Tranché : (b), avec un socle d'auteur

La SceneDef porte un découpage de référence : liste ordonnée de ShotDef, chacun avec une priorite (indispensable / souhaite / confort). Ce n'est pas ce qu'on tournera, c'est le vocabulaire disponible. Quand la scène entre au plan de travail, le moteur émet realisation.decoupage.propose : tous les indispensable, tous les souhaite, et des confort au prorata de l'ambition et du moral du réalisateur.

Réalisateur : « J'ai découpé la 12 en 14 plans. »
Joueur : « On a 4 heures. Ça fait 6 plans. »

Ce qui se joue là :

  • 6 plans, c'est un autre film — pas un film moins bon. Couper les plans de coupe, c'est perdre la montabilité (§8) ; couper le contrechamp, c'est perdre la scène.
  • Le réalisateur défend chaque plan (argumentaire) et sa satisfaction (§9) enregistre chaque refus.
  • Le joueur peut acheter du temps : heures sup (chapitre 6 + majoration), report (tournage.plan.reporte), ou tourner quand même et exploser le planning.

Pourquoi c'est le levier du tournage : le budget se dépense en temps, et le temps se dépense en plans. Recrutement, matériel, décors ne changent que le prix au plan. Seule cette décision change le nombre de plans — et elle se prend dix fois par jour, sous pression. L'option (c) est rejetée : un plan sans intention d'auteur ne peut ni porter un événement scénarisé, ni servir de leçon.


3. ShotDef / ShotPlan

Trois objets, trois moments : ShotDef (design-time, le découpage de référence), ShotPlan (runtime, avant le tournage — ce qui a survécu à la négociation et à l'affectation ; c'est l'entrée de la résolution), ShotState (§7).

type ValeurDePlan =
| 'plan_general' | 'plan_ensemble' | 'plan_large' | 'plan_moyen' | 'plan_americain'
| 'plan_rapproche' | 'gros_plan' | 'tres_gros_plan'
| 'insert' // objet, main, montre — se tourne vite, sauve un montage

type Mouvement =
| 'fixe' | 'panoramique' | 'travelling' | 'travelling_circulaire'
| 'grue' | 'steadicam' | 'epaule' | 'drone' | 'plan_sequence'

type RoleCouverture =
| 'master' // le plan-maître qui couvre toute la scène
| 'champ' | 'contrechamp' | 'insert' | 'plan_de_coupe'
| 'raccord_mouvement' // entrée/sortie de champ, raccord dans l'axe
| 'ambiance'

type Critere = 'jeu' | 'image' | 'son' | 'credibilite' | 'mise_en_scene'

type ShotDef = ContentBase & {
kind: 'shot'
sceneId: string // -> SceneDef.id
numero: string // "12-4" — la numérotation du dépouillement
priorite: 'indispensable' | 'souhaite' | 'confort'
argumentaire: string // ce que le réalisateur dira pour le défendre
valeur: ValeurDePlan
mouvement: Mouvement
roles: RoleCouverture[] // un master peut aussi valoir raccord_mouvement
personnagesCadres: string[] // -> RoleDef.id (le rôle, pas la personne)
dureeEcran_s: number
/** Éléments propres à CE plan, au-delà du dépouillement de la scène. */
elementsRequis: Array<{ elementDefId: string; poste: Poste; critique: boolean }>
/** Postes qui pèsent sur chaque critère, pour CE plan. Σ poids = 1 par critère. */
contributions: Partial<Record<Critere, Array<{ poste: Poste; poids_p: number }>>>
difficulte: number // 0–100 — ce que le plan EXIGE
ambition: number // 0–100 — ce qu'il peut donner AU MIEUX
installation_min: number
tournage_min: number // par prise, hors remise en place
prisesEsperees: number
risques: string[] // -> EventDef.id éligibles sur ce plan
}

type ShotPlan = InstanceBase & {
defId: string // -> ShotDef.id
dayId: string
ordreDansJournee: number
heurePrevue_min: number // minutes depuis le début de la journée
affectations: Array<{ poste: Poste; personInstanceId?: string; elementInstanceId?: string }>
/** Ajustements négociés : un travelling devenu un pano, ça se paie ailleurs. */
substitutions: Array<{ champ: 'mouvement' | 'valeur'; de: string; vers: string }>
budgetAlloue_eur: number
/** Annoncés par la réalisation, donc menteurs. Cf. §4 du README. */
installationEstimee: Rated
prisesEstimees: Rated
negocie: { demandeInitiale: number; accorde: number; refusesIds: string[] }
}

Ce qui ment : installationEstimee, prisesEstimees (runtime, Rated). Ce qui est honnête : difficulte, ambition, dureeEcran_s, les poids de contribution — l'auteur écrit la vérité, c'est le jeu qui la déguise.


4. Le modèle de résolution

Une fonction pure : resoudrePrise(shotPlan, monde, conditionsDuJour, numeroPrise, rng) → { qualites, minutes, cout_eur, evenements, journal }.

4.1 Constantes

const K = {
/** Exigence : difficulté 0 → il suffit d'être moyen ; 100 → il faut être excellent. */
EXIGENCE_PLANCHER: 0.35, EXIGENCE_PENTE: 0.65, // = PLANCHER + PENTE * difficulte/100
/** Plafond : un insert de main ne sera jamais un plan magnifique. */
PLAFOND_BASE: 45, PLAFOND_AMBITION: 55, // = BASE + AMBITION * ambition/100
/** Maillon faible : moyenne de puissance p<0. −2 = un contributeur à 0.30 plaque
* l'ensemble vers 0.45 même entouré d'excellence. */
P_MAILLON_FAIBLE: -2, EFF_PLANCHER: 0.05, // plancher : évite la division par 0
/** Fatigue : −30 % au maximum, courbe convexe (les 30 premiers points sont gratuits). */
FATIGUE_MALUS_MAX: 0.30, FATIGUE_EXPOSANT: 1.4,
/** Moral : bilatéral. Un moral au plafond fait mieux que le CV. 0.85 .. 1.15 */
MORAL_PLANCHER: 0.85, MORAL_AMPLITUDE: 0.30,
/** Pression horaire : rien avant 8 h, saturation à 13 h. Globale au plateau. */
PRESSION_SEUIL_H: 8, PRESSION_AMPLI_H: 5, PRESSION_MALUS_MAX: 0.35, PRESSION_EXPOSANT: 1.5,
/** Aléa : bruit multiplicatif borné à ±12 %, jamais plus, et tracé dans le journal. */
BRUIT_SIGMA: 0.06, BRUIT_BORNE: 0.12,
/** Grâce : l'accident heureux. Sans ça, le jeu n'est que punitif. */
GRACE_P_BASE: 0.03, GRACE_P_TALENT: 0.10, GRACE_P_PAR_PRISE: 0.02,
GRACE_P_FATIGUE: -0.08, GRACE_P_MAX: 0.18, GRACE_BONUS_MIN: 12, GRACE_BONUS_MAX: 25,
/** Comédiens : apprentissage puis usure au fil des prises (§6). */
APPRENTISSAGE_TAU: 2.5, APPRENTISSAGE_MAX: 0.15, USURE_PENTE: 0.05,
REMISE_EN_PLACE_MIN: 3,
} as const

4.2 Modulateurs individuels

/** Compétence effective d'un contributeur, 0..1, avant confrontation à l'exigence. */
function efficacite(p: PersonState, critere: Critere, ctx: Contexte): number {
const brut = p.stats[critere].actual / 100 // TOUJOURS actual
const fatigue = 1 - K.FATIGUE_MALUS_MAX * (p.fatigue / 100) ** K.FATIGUE_EXPOSANT
const moral = K.MORAL_PLANCHER + K.MORAL_AMPLITUDE * (p.moral / 100)
const pression = 1 - K.PRESSION_MALUS_MAX
* clamp01((ctx.heuresEcoulees - K.PRESSION_SEUIL_H) / K.PRESSION_AMPLI_H)
** K.PRESSION_EXPOSANT
return brut * fatigue * moral * pression
}
// Éléments (décor, caméra) : mêmes contributions, sans fatigue ni moral —
// efficaciteElement = stats[critere].actual / 100 * etat / 100

La pression frappe tout le plateau en même temps, et c'est voulu : tourner à 21 h après 11 h de plateau ne dégrade pas un poste, ça dégrade le film.

4.3 Le maillon faible

/** Moyenne de puissance pondérée. p = −2 → quasi-min doux. */
function agregerMaillonFaible(parts: Array<{ x: number; poids_p: number }>, p = K.P_MAILLON_FAIBLE) {
const s = parts.reduce((a, c) => a + c.poids_p * Math.max(c.x, K.EFF_PLANCHER) ** p, 0)
return s ** (1 / p)
}

Pourquoi pas une moyenne pondérée : un chef op à 88 et un travelling posé à l'arrache par un machiniste à 55 ne donnent pas « une image correcte », ils donnent une image qui tremble. Le spectateur voit le tremblement, pas le chef op. La moyenne de puissance négative encode ça avec une seule constante, reste lisse et dérivable pour l'équilibrage.

4.4 La résolution

function resoudrePrise(plan: ShotPlan, def: ShotDef, monde: MondeState,
jour: ConditionsDuJour, numeroPrise: number, rng: Rng): ResultatPrise {
const exigence = K.EXIGENCE_PLANCHER + K.EXIGENCE_PENTE * (def.difficulte / 100)
const plafond = K.PLAFOND_BASE + K.PLAFOND_AMBITION * (def.ambition / 100)
const journal: LigneJournal[] = []
const qualites = {} as Record<Critere, number>

for (const [critere, contributeurs] of entries(def.contributions)) {
// 1. compétence de chaque contributeur, confrontée à ce que le plan exige
const parts = contributeurs.map(c => {
const acteur = resoudreAffectation(plan, c.poste) // personne OU élément
const eff = efficacite(acteur, critere, { heuresEcoulees: jour.heuresEcoulees })
journal.push({ critere, poste: c.poste, brut: acteur.stats[critere].actual, eff })
return { x: clamp01(eff / exigence), poids_p: c.poids_p }
})
let q = agregerMaillonFaible(parts) // 2. maillon faible
if (critere === 'jeu') q *= 1 + courbeDePrises(plan, numeroPrise) // 3. prises (§6)
q *= plafond // 4. plafond d'ambition
q *= jour.modificateurs[critere] ?? 1 // 5. météo/nuit/bruit : 0.75–1.10
const bruit = clamp(rng.gaussien('bruit:' + critere) * K.BRUIT_SIGMA,
-K.BRUIT_BORNE, K.BRUIT_BORNE) // 6. aléa borné et tracé
q *= 1 + bruit
journal.push({ critere, facteur: 'alea', valeur: bruit })
qualites[critere] = clamp(Math.round(q), 0, 100)
}

// 7. la grâce — l'accident heureux
const pGrace = clamp(K.GRACE_P_BASE
+ K.GRACE_P_TALENT * (talentMax(plan) / 100)
+ K.GRACE_P_PAR_PRISE * (numeroPrise - 1)
+ K.GRACE_P_FATIGUE * (fatigueMoyenne(plan) / 100), 0, K.GRACE_P_MAX)
const evenements: EvenementSortant[] = []
if (rng.uniforme('grace') < pGrace) {
const cible = rng.choix('grace:critere', criteresDe(def))
const gain = Math.round(K.GRACE_BONUS_MIN
+ rng.uniforme('grace:gain') * (K.GRACE_BONUS_MAX - K.GRACE_BONUS_MIN))
qualites[cible] = clamp(qualites[cible] + gain, 0, 100)
evenements.push({ type: 'tournage.plan.grace_obtenue', critere: cible, gain })
}
// 8. incidents — le miroir de la grâce, tirés dans def.risques
for (const id of def.risques)
if (rng.uniforme('incident:' + id) < probaContextuelle(monde.catalogueEvents[id], plan, jour))
evenements.push({ type: 'tournage.plan.incident_survenu', eventDefId: id })

// 9. temps réellement consommé — la logistique (1er assistant, machino, régie) le pilote
const facteurInst = 1 + 0.6 * (1 - competenceLogistique(plan))
const minutes = (numeroPrise === 1 ? def.installation_min * facteurInst : K.REMISE_EN_PLACE_MIN)
+ def.tournage_min + minutesPerduesParIncidents(evenements)
// 10. argent réellement dépensé
const majoration = jour.heuresEcoulees > 8 ? 1.25 : 1 // heures sup
const cout_eur = Math.round(minutes * monde.burnPlateau_eur_par_min * majoration)
+ consommableParPrise_eur(monde.support)
return { qualites, minutes, cout_eur, evenements, journal }
}

Le journal n'est pas du debug : c'est ce que l'UI affiche après le plan (« image 44 : chef op 0.82, machinerie 0.44, électro 0.60 → maillon faible 0.60 ; nuit ×0.93 ; aléa −6 % »). Un raté doit être explicable en une ligne. C'est aussi la matière première du débrief (§9).

4.5 Exemple numérique complet — plan S12-P4

Scène 12, rupture, nuit, appartement loué non traité acoustiquement. Gros plan sur Marion, léger travelling avant. difficulte 70, ambition 70, installation_min 45, tournage_min 4. Il est 21 h 30, soit 11,5 h de plateau.

exigence = 0.35 + 0.65 × 0.70 = 0.805 plafond = 45 + 55 × 0.70 = 83.5
pression = clamp01((11.5 − 8)/5) = 0.70 → ×(1 − 0.35 × 0.70^1.5) = ×0.795 (tout le monde)
contributeurcritèreactualfatiguemoraleff÷ exigence
Marion (comédienne)jeu8255 → ×0.87040 → ×0.970.5500.683
Réalisateur (direction)jeu7560 → ×0.85355 → ×1.0150.5160.641
Scriptejeu6550 → ×0.88660 → ×1.030.4720.586
Chef opimage8845 → ×0.90265 → ×1.0450.6590.819
Chef machinisteimage5570 → ×0.81845 → ×0.9850.3520.438
Chef électroimage7055500.4840.602
Ingé sonson7250550.5150.639
Perchmanson4565400.2900.360
Appartement (isolation)son300.3000.373
Chef décocredibilite6050600.4350.541
Costumescredibilite6845600.5020.624
HMCcredibilite7455550.5200.645

Agrégation, prise 3 :

critèrepoidsmaillon faible (p=−2)(moyenne pondérée)×83.5×conditions×aléaq
jeu.60 / .30 / .100.6590.66455.0×0.97 nuit, décor exigu+5 %56
image.45 / .25 / .300.5970.65949.9×0.93 nuit, peu de lumière−6 %44
son.60 / .25 / .150.4730.55639.5×0.88 rue passante+2 %35
credibilite.50 / .25 / .250.5820.58648.6×1.00−1 %48

Lecture : sur image, la moyenne pondérée dirait 0.659, le maillon faible dit 0.597 — le chef op à 0.82 ne rachète pas le travelling à 0.44. Sur son, l'écart est de 8 points de qualité finale : le perchman à 0.36 et le décor à 0.37 s'additionnent en catastrophe.

Grâce : p = 0.03 + 0.10×0.88 + 0.02×2 − 0.08×0.55 = 0.114 ; tirage prise 3 = 0.62 → rien. Prise 5 : p = 0.106, tirage 0.07grâce sur jeu, +14 → la prise 5 monte à jeu 70.

installation 45 min × 1.27 (logistique 0.55) = 57 min
5 prises 5 × (4 min tournage + 3 min remise) = 35 min
TOTAL = 92 min (1 h 32)
burn plateau 21 €/min × 1.25 (heures sup après 8 h) = 26,25 €/min
temps 92 × 26,25 = 2 415 €
consommables 5 × 12 € = 60 €
TOTAL = 2 475 €

Un gros plan de 6 secondes à l'écran coûte 2 475 € et 1 h 32. C'est ce chiffre, affiché après chaque plan, qui fait comprendre « on n'a le temps que pour 6 plans ».

Perçu vs réel : sur le plateau le réalisateur dit « on l'a » — jeu claimed 78 / actual 70, son claimed 60 / actual 35 (personne n'écoute vraiment au casque à 21 h 30). Le son sera découvert inexploitable au visionnage, deux ticks plus tard (§7).


5. Le déterminisme

La partie est event-sourcée : l'état est dérivé du journal. Rejouer le journal doit redonner exactement le même film — sinon pas de reprise de session, pas de débrief contrefactuel (§9), pas de bug reproductible, pas de seed partageable. Donc resoudrePrise est pure : mêmes entrées → mêmes sorties, aucun effet de bord, aucune horloge, aucune entropie ambiante.

/** Seedé par (partie, plan, prise, canal). Un canal = un usage nommé. */
function rngPourPrise(seedPartie: string, shotInstanceId: string, prise: number): Rng {
return (canal: string) => sfc32(hash32(`${seedPartie}|${shotInstanceId}|${prise}|${canal}`))
}

Les canaux nommés ('bruit:image', 'grace', 'incident:panne-groupe'…) sont le point crucial : chaque canal a son propre flux. Sans eux, ajouter un tirage en v0.4 décalerait tous les tirages suivants et toutes les parties enregistrées deviendraient fausses. Avec eux, un nouveau tirage = un nouveau canal, les anciens ne bougent pas.

Règles à faire respecter par le linter, dans engine/ :

  • aucun Math.random()no-restricted-globals / no-restricted-properties, erreur bloquante ;
  • aucun Date.now() ni new Date() : le temps du jeu est le Tick, il vient de l'état ;
  • aucune dépendance à l'ordre d'itération d'un Set/Map ni à un sort() instable — trier par ULID ;
  • aucun crypto.randomUUID() : les ULID d'instance viennent de (seed, tick, compteur) ;
  • entrées readonly : la résolution ne mute rien, elle émet des événements, le réducteur applique.

Test d'or en CI sur un corpus de parties enregistrées : rejouer(journal) === etatFinal. Toute dérive fait échouer le build.


6. Les prises

type Take = InstanceBase & {
shotInstanceId: string
numero: number
demandeePar: 'realisateur' | 'comedien' | 'technique' | 'production'
motif: 'premiere' | 'jeu' | 'technique' | 'securite' | 'incident' | 'caprice'
qualites: Record<Critere, number> // le réel — masqué jusqu'au visionnage
appreciationPlateau: Rated // ce que le réalisateur croit en avoir tiré
minutes: number
cout_eur: number
circled: boolean // « la bonne » — la prise annoncée au montage
defauts: Defaut[] // découverts plus tard (§7)
}

/** Retour sur la prise n, appliqué au critère `jeu`. Apprentissage puis usure. */
function courbeDePrises(plan: ShotPlan, n: number): number {
const seuil = 4 + enduranceMoyenneComediens(plan) / 25 // 4 à 8 prises
const apprend = K.APPRENTISSAGE_MAX * (1 - Math.exp(-(n - 1) / K.APPRENTISSAGE_TAU))
const use = Math.max(0, n - seuil) * K.USURE_PENTE
return apprend - use
}

Pour une endurance 50 (seuil = 6) :

prise123456810
bonus jeu0 %+5,0 %+9,2 %+11,8 %+13,0 %+13,8 %+4,5 %−5,4 %

L'optimum est vers la 6ᵉ et dépend du comédien : Marion (endurance 30, seuil 5,2) casse à la 7ᵉ, un comédien de théâtre (endurance 85, seuil 7,4) tient jusqu'à 11. Le joueur ne connaît pas l'endurance réelle — c'est un Rated, il l'apprend en observant. Chaque prise émet aussi +3 fatigue aux comédiens cadrés, +1 à l'équipe, et au-delà du seuil −2 moral au comédien (« il ne me fait pas confiance »).

La négociation « on la garde ? »

  1. tournage.prise.effectuee — le moteur résout.
  2. realisation.prise.demandee — le réalisateur en demande une autre, avec un motif et un gain estimé (Rated : il surestime toujours).
  3. Le joueur répond :
    • accepter → nouvelle prise, temps et argent consommés ;
    • refuserproduction.prise.refusee : −4 satisfaction réalisateur, −2 moral du comédien, le plan est figé sur la meilleure prise connue ;
    • « une dernière pour la sécurité » → une prise, motif securite, coût plein mais +6 satisfaction réalisateur. C'est la concession qui achète la paix — et qui, cumulée sur 40 plans, coûte une journée de tournage entière.

La prise circled est choisie par le réalisateur sur appreciationPlateau, pas sur qualites. Il se trompe régulièrement : le montage découvrira parfois qu'une prise non cerclée était meilleure — mais seulement si le dérushage a été fait sérieusement.


7. ShotState et le cycle de vie en post

Un plan tourné n'est pas fini. Il traverse une chaîne où la vérité se révèle par vagues — c'est le principal levier de tension d'après-tournage.

type StatutPlan =
| 'prevu' | 'prepare' | 'en_cours' // plateau
| 'tourne' | 'reporte' | 'abandonne'
| 'rushes_recus' | 'visionne' | 'derushe' // post
| 'monte' | 'vfx' | 'etalonne' | 'mixe' | 'livre'
| 'a_retourner' // le cauchemar

type NatureDefaut =
| 'flou_focus' | 'perche_visible' | 'micro_dans_le_champ' | 'raccord_faux'
| 'son_inexploitable' | 'voile' | 'carte_corrompue'
| 'figurant_regarde_camera' | 'anachronisme'

type Defaut = {
nature: NatureDefaut
gravite: number // 0–100
critereTouche: Critere
detectableA: 'plateau' | 'visionnage' | 'montage' | 'etalonnage' | 'jamais'
decouvertAuTick?: Tick
reparable: 'non' | 'vfx' | 'postsynchro' | 'montage' | 'retournage'
coutReparation_eur: number
}

type ShotState = InstanceBase & {
planId: string
statut: StatutPlan
takes: Take[]
takeRetenueId?: string
qualitesPercues: Record<Critere, Rated> // ce que le joueur voit
qualitesReelles: Record<Critere, number> // jamais sérialisé avant révélation
defauts: Defaut[]
delaiRushes_ticks: number // labo / DIT / transfert
apportsPost: Partial<Record<Critere, number>>
minutesConsommees: number
cout_eur: number
}
vaguequandce qui se révèlepar qui
1. plateauimmédiatappreciationPlateau (optimiste) ; défauts detectableA: 'plateau' avec une proba liée à la compétence du DIT et de la scriptele plateau
2. visionnagetick + delaiRushes_ticks (1 à 4)les vraies valeurs d'image + les défauts 'visionnage'post.rushes.visionnesle labo
3. montagequand la scène est montéejeu, mise_en_scene, credibilite, son, raccords faux, trous de couverturele monteur

C'est délibérément asymétrique : l'image est jugée vite, le jeu est jugé tard. Le joueur découvre au montage — décor rendu, comédiens partis — que la scène ne fonctionne pas. Un plan peut passer a_retourner : une journée entière (rappel des comédiens, remontage du décor, avenants), souvent impossible, donc la scène est amputée.

Le delaiRushes_ticks est un choix de production, pas un réglage : rushes vus le soir même (coûte du temps d'équipe, révèle vite) ou en fin de semaine (gratuit — et on découvre le vendredi qu'on a raté lundi, décor déjà rendu).

La post modifie les qualités, elle ne les crée pas :

étapecritèreapport
dérushagejeurévèle une meilleure prise non cerclée : +0 à +6
montagemise_en_scene−20 à +12, selon la couverture (§8)
VFXcredibilite+15 (efface une perche, un anachronisme), ou −10 si mal fait
étalonnageimage+8 (rattrape une expo, unifie une scène)
mixage / postsynchroson+20, mais jeu −8

Événements : post.rushes.recus, post.rushes.visionnes, post.plan.defaut_decouvert, post.plan.derushe, montage.plan.monte, post.plan.etalonne, post.plan.mixe, production.plan.retournage_decide.


8. Agrégation plan → scène → film

8.1 D'abord la couverture — c'est un portail, pas un bonus

Une scène avec trois plans magnifiques et pas de contrechamp est immontable.

type CouvertureRequise = {
roles: Array<{ role: RoleCouverture; poids_p: number; obligatoire: boolean }>
dureeEcran_s: number
}

function evaluerCouverture(scene: SceneDef, plans: ShotState[]): Couverture {
const utilisables = plans.filter(p => p.statut !== 'abandonne'
&& !p.defauts.some(d => d.reparable === 'non'))
const couverts = new Set(utilisables.flatMap(rolesDe))
const manquantsObligatoires = scene.couverture.roles.filter(r => r.obligatoire && !couverts.has(r.role))
const couverture_p = scene.couverture.roles.filter(r => couverts.has(r.role))
.reduce((s, r) => s + r.poids_p, 0)
const ratioDuree = clamp01(sommeDureeUtile(utilisables) / scene.couverture.dureeEcran_s)
return { couverture_p, ratioDuree, manquantsObligatoires,
montable: manquantsObligatoires.length === 0 && ratioDuree >= 0.9 }
}

Si montable === falsemontage.scene.couverture_insuffisante. La scène n'est pas perdue : le monteur triche (plan large tenu trop longtemps, voix off, coupe sèche). Coût : mise_en_scene plafonnée à 55 × couverture_p, +1 tick de montage. Le joueur voit la couverture en temps réel pendant le tournage, sous forme d'une grille de rôles : refuser le contrechamp de la 12 allume une case rouge immédiatement. C'est la leçon de métier centrale de la fiche.

8.2 Deux modes d'agrégation selon le critère

Le montage choisit — mais il ne peut pas cacher un défaut visible.

critèremodepourquoi
jeu, mise_en_scenemoyenne des 60 % meilleurs plans, pondérée par la durée à l'écranle montage garde le meilleur et coupe le reste
imagemoyenne pondérée par la durée, écrêtée par la varianceune image inégale d'un plan à l'autre se voit
son, credibilitemaillon faible, p = −3 (plus dur qu'au plan)une perche visible ruine la scène entière
function agregerScene(scene: SceneDef, plans: ShotState[]): QualiteScene {
const c = evaluerCouverture(scene, plans)
const q = {} as Record<Critere, number>
q.jeu = moyenneDesMeilleurs(plans, 'jeu', 0.6)
q.mise_en_scene = moyenneDesMeilleurs(plans, 'mise_en_scene', 0.6)
q.image = moyennePonderee(plans, 'image') - 0.5 * ecartType(plans, 'image')
q.son = agregerMaillonFaible(parts(plans, 'son'), -3)
q.credibilite = agregerMaillonFaible(parts(plans, 'credibilite'), -3)
if (!c.montable) q.mise_en_scene = Math.min(q.mise_en_scene, 55 * c.couverture_p)
return { ...q, couverture: c }
}

8.3 Scène → film

Le film agrège les scènes pondérées par leur importance dramatique (SceneDef.poidsDramatique, 0–100), pas par leur durée : une scène de 40 s peut porter le film. Même dichotomie qu'au-dessus — jeu et mise_en_scene en moyenne pondérée, son et credibilite en maillon faible sur les scènes. Une scène immontable retirée du film ampute le récit : montage.film.trou_narratif, malus global sur mise_en_scene.


9. Le film final et le score

9.1 La VisionDef — définie au départ, connue du joueur, jamais négociable

type VisionDef = ContentBase & {
kind: 'vision'
cibles: Record<Critere, number> // 0–100 : le film idéal
sens: Record<Critere, 'plancher' | 'exact'> // dépasser : toléré ou non ?
poids_p: Record<Critere, number> // Σ = 1 : ce à quoi il tient
toleranceInegalite: number // 0–100
scenesSacrees: string[] // -> SceneDef.id
phrase: string // « Je veux qu'on entende respirer. »
}

/** 0–100. Distance L1 pondérée à la vision + pénalité d'irrégularité. */
function alignement(film: Record<Critere, number>, v: VisionDef): number {
const distance = criteres.reduce((s, c) => {
const ecart = v.sens[c] === 'plancher'
? Math.max(0, v.cibles[c] - film[c]) // dépasser ne fâche pas
: Math.abs(film[c] - v.cibles[c]) // le brut voulu reste brut
return s + v.poids_p[c] * ecart / 100
}, 0)
const irregularite = ecartTypeScenes(film) * (1 - v.toleranceInegalite / 100) / 100
return clamp(Math.round(100 * (1 - distance - 0.4 * irregularite)), 0, 100)
}

Exemple, premier film d'auteur : cibles { jeu 85, image 70, son 80, credibilite 60, mise_en_scene 80 }, poids { jeu .35, son .25, mise_en_scene .25, image .10, credibilite .05 }. Sur ce film, économiser sur l'ingé son est un crime — et le joueur le sait dès la préparation.

9.2 Le bilan

type BilanFinal = {
noteArtistique: number // alignement + qualité brute
respectBudget: { prevu_eur: number; depense_eur: number; ecart_p: number; note: number }
respectPlanning: { joursPrevus: number; joursReels: number; note: number }
satisfactionRealisateur: number // alignement ×.60 + scenesSacrees ×.25 − prisesRefusees ×.15
satisfactionProducteur: number // respectBudget ×.45 + respectPlanning ×.30 + vendabilite ×.25
reputation: { avant: number; apres: number; parPoste: Partial<Record<Poste, number>> }
sortie: { salles: number; distinctions: string[]; presse: string }
debrief: DecisionCausale[]
}

Les deux satisfactions sont en tension par construction — c'est le jeu. On ne peut pas maximiser les deux ; on peut échouer aux deux. La réputation se ventile par poste (« bon avec les techniciens, infernal avec les comédiens ») et conditionne le recrutement de la partie suivante.

9.3 Le débrief pédagogique

C'est ce que le déterminisme (§5) rend possible, et c'est la vraie valeur du jeu.

type DecisionCausale = {
tick: Tick
eventId: string // la décision, dans le journal
libelle: string // « Chef op remplacé par un moins cher (−18 000 €) »
chaine: string[] // les événements qui en découlent, dans l'ordre
impact: Partial<Record<Critere, number>>
contrefactuel?: { // rejeu réel du journal, cette décision inversée, même seed
libelle: string; noteArtistique: number; depense_eur: number
}
}

Le contrefactuel n'est pas une estimation : on rejoue le journal avec un événement substitué, à seed identique. C'est exact, rapide (le moteur est pur) et incontestable. On en calcule 5 à 8, sur les décisions au plus fort impact absolu. La restitution suit la chaîne causale, pas la chronologie :

Note image : 48/100
└─ 22 plans sur 41 ont un maillon faible « machinerie »
└─ Chef machiniste engagé à 950 €/j (claimed 70, actual 55)
└─ Décision tick 14 : arbitrage « machinerie » vs « 3 jours de plateau en plus »
└─ Contrefactuel : machiniste à 1 400 €/j → image 63, budget +12 600 €,
planning inchangé, note artistique 61 au lieu de 54.

10. Questions ouvertes

  1. Le joueur voit-il les critères par plan ou par scène ? Par plan c'est lisible mais transforme le tournage en tableur ; par scène c'est réaliste mais le lien décision → conséquence se distend. Piste : par plan pendant le tournage en Rated flou, chiffré seulement après visionnage.
  2. ambition est-elle globale ou par critère ? Un insert de main peut être un plan de credibilite exceptionnel et nul en jeu. Partial<Record<Critere, number>> serait plus juste, au prix d'un travail d'auteur bien plus lourd par plan.
  3. P_MAILLON_FAIBLE = −2 est-il jouable ? Sévère : il rend presque impossible de dépasser 70 sur un critère à cinq contributeurs. Assouplir à −1.5, ou faire varier p par critère (dur sur son, doux sur jeu) ?
  4. Les prises se négocient-elles une par une, ou par enveloppe ? Une-par-une est fidèle et tendu mais fait 200 micro-décisions par jour ; l'enveloppe (« 5 max sur cette scène ») est jouable mais perd le réalisateur qui insiste. Piste : une-par-une + une règle « toujours accepter jusqu'à n » réglable.
  5. Quelle granularité pour la post ? Le §7 la modélise par plan, fidèle mais 300 objets à gérer en fin de partie. Basculer la post à la scène et ne garder le plan que pour les défauts et les VFX ?
  6. La grâce doit-elle être annoncée ? « +14, coup de chance » est lisible mais casse l'illusion ; non annoncée, la résolution devient opaque. Piste : annoncée narrativement (« Marion a fait quelque chose que personne n'attendait »), chiffrée dans le débrief seul.
  7. Le contrefactuel est-il abusable ? S'il existe en fin de partie, pourquoi pas un « annuler » en cours de partie ? Où passe la frontière entre outil pédagogique et triche ?
  8. difficulte et ambition : deux champs ou un seul ? Fortement corrélées dans les données d'auteur ; les séparer double la charge d'écriture pour un gain à démontrer.