AP6 (restes) : catalogue des animations (Catalogue.py, pack.json, catalogue.html/json) et planches animées du mod Quaternius
Phase AP6 (CHG-210, 211). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ad75b08d06
commit
143e29643b
5 changed files with 4388 additions and 1 deletions
607
AnimSources/quaternius/pack.json
Normal file
607
AnimSources/quaternius/pack.json
Normal file
|
|
@ -0,0 +1,607 @@
|
|||
{
|
||||
"pack": "quaternius",
|
||||
"prefixe": "qua",
|
||||
"source": "Quaternius Universal Animation Library (Standard)",
|
||||
"auteur": "Quaternius",
|
||||
"licence": "CC0 1.0",
|
||||
"url": "https://quaternius.com",
|
||||
"fichiers": [
|
||||
"D:/Game/Universal Animation Library[Standard]/Unreal-Godot/UAL1_Standard.glb"
|
||||
],
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"exclus": [
|
||||
"A_TPose"
|
||||
],
|
||||
"animations": {
|
||||
"qua_crouch_fwd_loop": {
|
||||
"clip": "Crouch_Fwd_Loop",
|
||||
"nom": {
|
||||
"fr": "Marche accroupie",
|
||||
"en": "Crouch walk"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de marche accroupie sur place ; à combiner avec un déplacement pour une scène furtive.",
|
||||
"en": "Looping crouched walk in place; combine with a character movement for a sneaking scene."
|
||||
}
|
||||
},
|
||||
"qua_crouch_idle_loop": {
|
||||
"clip": "Crouch_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente accroupie",
|
||||
"en": "Crouch idle"
|
||||
},
|
||||
"categorie": "attente",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente accroupie, pour se cacher ou observer discrètement.",
|
||||
"en": "Looping crouched idle, for hiding or watching discreetly."
|
||||
}
|
||||
},
|
||||
"qua_dance_loop": {
|
||||
"clip": "Dance_Loop",
|
||||
"nom": {
|
||||
"fr": "Danse",
|
||||
"en": "Dance"
|
||||
},
|
||||
"categorie": "danse",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Boucle de danse libre, pour une scène de fête ou de club.",
|
||||
"en": "Looping free dance, for a party or club scene."
|
||||
}
|
||||
},
|
||||
"qua_death01": {
|
||||
"clip": "Death01",
|
||||
"nom": {
|
||||
"fr": "Mort",
|
||||
"en": "Death"
|
||||
},
|
||||
"categorie": "chute",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : le personnage s'effondre et reste au sol ; à utiliser pour une mort ou un évanouissement.",
|
||||
"en": "Played once: the character collapses and stays down; use for a death or a faint."
|
||||
}
|
||||
},
|
||||
"qua_driving_loop": {
|
||||
"clip": "Driving_Loop",
|
||||
"nom": {
|
||||
"fr": "Conduite",
|
||||
"en": "Driving"
|
||||
},
|
||||
"categorie": "conduite",
|
||||
"accessoire": "volant",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de conduite, mains sur le volant ; pour un personnage assis dans un véhicule.",
|
||||
"en": "Looping driving pose, hands on the wheel; for a character seated in a vehicle."
|
||||
}
|
||||
},
|
||||
"qua_fixing_kneeling": {
|
||||
"clip": "Fixing_Kneeling",
|
||||
"nom": {
|
||||
"fr": "Réparer à genoux",
|
||||
"en": "Fix kneeling"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de bricolage à genoux, pour une réparation ou un travail au sol.",
|
||||
"en": "Looping kneeling repair work, for fixing something or working at ground level."
|
||||
}
|
||||
},
|
||||
"qua_hit_chest": {
|
||||
"clip": "Hit_Chest",
|
||||
"nom": {
|
||||
"fr": "Coup à la poitrine",
|
||||
"en": "Hit in chest"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : réaction à un coup reçu au torse ; à placer après une attaque.",
|
||||
"en": "Played once: reaction to a blow to the chest; place it after an attack."
|
||||
}
|
||||
},
|
||||
"qua_hit_head": {
|
||||
"clip": "Hit_Head",
|
||||
"nom": {
|
||||
"fr": "Coup à la tête",
|
||||
"en": "Hit in head"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : réaction à un coup reçu à la tête ; à placer après une attaque.",
|
||||
"en": "Played once: reaction to a blow to the head; place it after an attack."
|
||||
}
|
||||
},
|
||||
"qua_idle_loop": {
|
||||
"clip": "Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente",
|
||||
"en": "Idle"
|
||||
},
|
||||
"categorie": "attente",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente debout par défaut, pour un personnage qui patiente.",
|
||||
"en": "Default standing idle loop, for a waiting character."
|
||||
}
|
||||
},
|
||||
"qua_idle_talking_loop": {
|
||||
"clip": "Idle_Talking_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente en parlant",
|
||||
"en": "Idle talking"
|
||||
},
|
||||
"categorie": "conversation",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle debout avec gestes de la main, pour un personnage qui parle dans un dialogue.",
|
||||
"en": "Standing loop with hand gestures, for a character speaking in a dialogue."
|
||||
}
|
||||
},
|
||||
"qua_idle_torch_loop": {
|
||||
"clip": "Idle_Torch_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente avec torche",
|
||||
"en": "Idle with torch"
|
||||
},
|
||||
"categorie": "attente",
|
||||
"accessoire": "torche",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente debout en tenant une torche ; pour une scène de nuit ou d'exploration.",
|
||||
"en": "Standing idle loop holding a torch; for a night or exploration scene."
|
||||
}
|
||||
},
|
||||
"qua_interact": {
|
||||
"clip": "Interact",
|
||||
"nom": {
|
||||
"fr": "Interagir",
|
||||
"en": "Interact"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : geste pour actionner un objet (bouton, porte, levier).",
|
||||
"en": "Played once: gesture to operate an object (button, door, lever)."
|
||||
}
|
||||
},
|
||||
"qua_jog_fwd_loop": {
|
||||
"clip": "Jog_Fwd_Loop",
|
||||
"nom": {
|
||||
"fr": "Jogging",
|
||||
"en": "Jog"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de course légère sur place ; à combiner avec un déplacement du personnage.",
|
||||
"en": "Looping light jog in place; combine with a character movement."
|
||||
}
|
||||
},
|
||||
"qua_jump_land": {
|
||||
"clip": "Jump_Land",
|
||||
"nom": {
|
||||
"fr": "Réception de saut",
|
||||
"en": "Jump landing"
|
||||
},
|
||||
"categorie": "chute",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : réception au sol ; dernière phase d'un saut (après la boucle en l'air).",
|
||||
"en": "Played once: landing on the ground; last phase of a jump (after the airborne loop)."
|
||||
}
|
||||
},
|
||||
"qua_jump_loop": {
|
||||
"clip": "Jump_Loop",
|
||||
"nom": {
|
||||
"fr": "Saut en l'air",
|
||||
"en": "Jump airborne"
|
||||
},
|
||||
"categorie": "chute",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de la phase en l'air d'un saut, entre le départ et la réception.",
|
||||
"en": "Looping airborne phase of a jump, between the start and the landing."
|
||||
}
|
||||
},
|
||||
"qua_jump_start": {
|
||||
"clip": "Jump_Start",
|
||||
"nom": {
|
||||
"fr": "Départ de saut",
|
||||
"en": "Jump start"
|
||||
},
|
||||
"categorie": "chute",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : impulsion au départ d'un saut ; première phase avant la boucle en l'air.",
|
||||
"en": "Played once: take-off impulse; first phase before the airborne loop."
|
||||
}
|
||||
},
|
||||
"qua_pickup_table": {
|
||||
"clip": "PickUp_Table",
|
||||
"nom": {
|
||||
"fr": "Ramasser sur une table",
|
||||
"en": "Pick up from table"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : le personnage prend un objet posé sur une table.",
|
||||
"en": "Played once: the character picks up an object lying on a table."
|
||||
}
|
||||
},
|
||||
"qua_pistol_aim_down": {
|
||||
"clip": "Pistol_Aim_Down",
|
||||
"nom": {
|
||||
"fr": "Viser vers le bas",
|
||||
"en": "Aim down"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Pose de visée pistolet vers le bas ; à enchaîner avec les autres visées ou le tir.",
|
||||
"en": "Pistol aiming pose pointing down; chain with the other aims or the shot."
|
||||
}
|
||||
},
|
||||
"qua_pistol_aim_neutral": {
|
||||
"clip": "Pistol_Aim_Neutral",
|
||||
"nom": {
|
||||
"fr": "Viser droit",
|
||||
"en": "Aim straight"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Pose de visée pistolet à l'horizontale ; pose de base avant un tir.",
|
||||
"en": "Pistol aiming pose at level; base pose before a shot."
|
||||
}
|
||||
},
|
||||
"qua_pistol_aim_up": {
|
||||
"clip": "Pistol_Aim_Up",
|
||||
"nom": {
|
||||
"fr": "Viser vers le haut",
|
||||
"en": "Aim up"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Pose de visée pistolet vers le haut ; à enchaîner avec les autres visées ou le tir.",
|
||||
"en": "Pistol aiming pose pointing up; chain with the other aims or the shot."
|
||||
}
|
||||
},
|
||||
"qua_pistol_idle_loop": {
|
||||
"clip": "Pistol_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente au pistolet",
|
||||
"en": "Pistol idle"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente debout, pistolet en main ; pour un personnage armé qui guette.",
|
||||
"en": "Standing idle loop with a pistol in hand; for an armed character on guard."
|
||||
}
|
||||
},
|
||||
"qua_pistol_reload": {
|
||||
"clip": "Pistol_Reload",
|
||||
"nom": {
|
||||
"fr": "Recharger le pistolet",
|
||||
"en": "Pistol reload"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : rechargement du pistolet ; à placer entre deux tirs.",
|
||||
"en": "Played once: pistol reload; place it between two shots."
|
||||
}
|
||||
},
|
||||
"qua_pistol_shoot": {
|
||||
"clip": "Pistol_Shoot",
|
||||
"nom": {
|
||||
"fr": "Tir au pistolet",
|
||||
"en": "Pistol shot"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : un tir de pistolet avec recul.",
|
||||
"en": "Played once: one pistol shot with recoil."
|
||||
}
|
||||
},
|
||||
"qua_punch_cross": {
|
||||
"clip": "Punch_Cross",
|
||||
"nom": {
|
||||
"fr": "Direct",
|
||||
"en": "Cross punch"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : coup de poing direct ; pour une bagarre.",
|
||||
"en": "Played once: cross punch; for a fight."
|
||||
}
|
||||
},
|
||||
"qua_punch_jab": {
|
||||
"clip": "Punch_Jab",
|
||||
"nom": {
|
||||
"fr": "Jab",
|
||||
"en": "Jab"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : coup de poing rapide ; pour une bagarre.",
|
||||
"en": "Played once: quick jab punch; for a fight."
|
||||
}
|
||||
},
|
||||
"qua_push_loop": {
|
||||
"clip": "Push_Loop",
|
||||
"nom": {
|
||||
"fr": "Pousser",
|
||||
"en": "Push"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de poussée à deux mains sur place ; pour pousser un meuble ou une porte lourde.",
|
||||
"en": "Looping two-handed push in place; for pushing furniture or a heavy door."
|
||||
}
|
||||
},
|
||||
"qua_roll": {
|
||||
"clip": "Roll",
|
||||
"nom": {
|
||||
"fr": "Roulade",
|
||||
"en": "Roll"
|
||||
},
|
||||
"categorie": "sport",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : roulade au sol ; pour esquiver ou plonger.",
|
||||
"en": "Played once: ground roll; for dodging or diving."
|
||||
}
|
||||
},
|
||||
"qua_sitting_enter": {
|
||||
"clip": "Sitting_Enter",
|
||||
"nom": {
|
||||
"fr": "S'asseoir",
|
||||
"en": "Sit down"
|
||||
},
|
||||
"categorie": "assis",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : entrée, le personnage passe de debout à assis ; à suivre d'une boucle assise.",
|
||||
"en": "Played once: entry, the character goes from standing to seated; follow with a seated loop."
|
||||
}
|
||||
},
|
||||
"qua_sitting_exit": {
|
||||
"clip": "Sitting_Exit",
|
||||
"nom": {
|
||||
"fr": "Se lever",
|
||||
"en": "Stand up"
|
||||
},
|
||||
"categorie": "assis",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : sortie, le personnage passe d'assis à debout ; à placer après une boucle assise.",
|
||||
"en": "Played once: exit, the character goes from seated to standing; place after a seated loop."
|
||||
}
|
||||
},
|
||||
"qua_sitting_idle_loop": {
|
||||
"clip": "Sitting_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Assis, attente",
|
||||
"en": "Seated idle"
|
||||
},
|
||||
"categorie": "assis",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente assise, après « s'asseoir » ; pour un personnage installé sur une chaise.",
|
||||
"en": "Seated idle loop, after the sit-down clip; for a character settled on a chair."
|
||||
}
|
||||
},
|
||||
"qua_sitting_talking_loop": {
|
||||
"clip": "Sitting_Talking_Loop",
|
||||
"nom": {
|
||||
"fr": "Assis, en parlant",
|
||||
"en": "Seated talking"
|
||||
},
|
||||
"categorie": "assis",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Boucle assise avec gestes de la main, pour un dialogue entre personnages assis.",
|
||||
"en": "Seated loop with hand gestures, for a dialogue between seated characters."
|
||||
}
|
||||
},
|
||||
"qua_spell_simple_enter": {
|
||||
"clip": "Spell_Simple_Enter",
|
||||
"nom": {
|
||||
"fr": "Début d'incantation",
|
||||
"en": "Spell start"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : mise en position pour lancer un sort ; à suivre de la boucle d'incantation.",
|
||||
"en": "Played once: stance to begin casting a spell; follow with the spell idle loop."
|
||||
}
|
||||
},
|
||||
"qua_spell_simple_exit": {
|
||||
"clip": "Spell_Simple_Exit",
|
||||
"nom": {
|
||||
"fr": "Fin d'incantation",
|
||||
"en": "Spell end"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : retour à la posture normale après un sort.",
|
||||
"en": "Played once: return to normal stance after a spell."
|
||||
}
|
||||
},
|
||||
"qua_spell_simple_idle_loop": {
|
||||
"clip": "Spell_Simple_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Incantation",
|
||||
"en": "Spell idle"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de concentration, mains levées, entre l'entrée et le lancer du sort.",
|
||||
"en": "Looping concentration pose, hands raised, between the entry and the cast."
|
||||
}
|
||||
},
|
||||
"qua_spell_simple_shoot": {
|
||||
"clip": "Spell_Simple_Shoot",
|
||||
"nom": {
|
||||
"fr": "Lancer un sort",
|
||||
"en": "Cast spell"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : le geste qui lance le sort ; à placer après la boucle d'incantation.",
|
||||
"en": "Played once: the gesture that casts the spell; place after the spell idle loop."
|
||||
}
|
||||
},
|
||||
"qua_sprint_loop": {
|
||||
"clip": "Sprint_Loop",
|
||||
"nom": {
|
||||
"fr": "Sprint",
|
||||
"en": "Sprint"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Boucle de course rapide sur place ; à combiner avec un déplacement pour une fuite ou une poursuite.",
|
||||
"en": "Looping fast run in place; combine with a movement for an escape or chase."
|
||||
}
|
||||
},
|
||||
"qua_swim_fwd_loop": {
|
||||
"clip": "Swim_Fwd_Loop",
|
||||
"nom": {
|
||||
"fr": "Nage",
|
||||
"en": "Swim"
|
||||
},
|
||||
"categorie": "nage",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de nage en avant ; à combiner avec un déplacement dans l'eau.",
|
||||
"en": "Looping forward swim; combine with a movement in water."
|
||||
},
|
||||
"posture": "dans l'eau"
|
||||
},
|
||||
"qua_swim_idle_loop": {
|
||||
"clip": "Swim_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Surplace dans l'eau",
|
||||
"en": "Treading water"
|
||||
},
|
||||
"categorie": "nage",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de flottaison sur place dans l'eau.",
|
||||
"en": "Looping treading water in place."
|
||||
},
|
||||
"posture": "dans l'eau"
|
||||
},
|
||||
"qua_sword_attack": {
|
||||
"clip": "Sword_Attack",
|
||||
"nom": {
|
||||
"fr": "Attaque à l'épée",
|
||||
"en": "Sword attack"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "épée",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : un coup d'épée ; à enchaîner après l'attente à l'épée.",
|
||||
"en": "Played once: a sword strike; chain after the sword idle."
|
||||
}
|
||||
},
|
||||
"qua_sword_idle": {
|
||||
"clip": "Sword_Idle",
|
||||
"nom": {
|
||||
"fr": "Attente à l'épée",
|
||||
"en": "Sword idle"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "épée",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente debout, épée en main, en garde.",
|
||||
"en": "Standing idle loop, sword in hand, on guard."
|
||||
}
|
||||
},
|
||||
"qua_walk_formal_loop": {
|
||||
"clip": "Walk_Formal_Loop",
|
||||
"nom": {
|
||||
"fr": "Marche formelle",
|
||||
"en": "Formal walk"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "formel",
|
||||
"usage": {
|
||||
"fr": "Boucle de marche tenue et digne sur place ; à combiner avec un déplacement (cérémonie, travail).",
|
||||
"en": "Looping upright, dignified walk in place; combine with a movement (ceremony, office)."
|
||||
}
|
||||
},
|
||||
"qua_walk_loop": {
|
||||
"clip": "Walk_Loop",
|
||||
"nom": {
|
||||
"fr": "Marche",
|
||||
"en": "Walk"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de marche sur place ; à combiner avec un déplacement du personnage.",
|
||||
"en": "Looping walk in place; combine with a character movement."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
ModSources/lpr_anim_quaternius/Module/Animations/catalogue.html
Normal file
106
ModSources/lpr_anim_quaternius/Module/Animations/catalogue.html
Normal file
File diff suppressed because one or more lines are too long
3187
ModSources/lpr_anim_quaternius/Module/Animations/catalogue.json
Normal file
3187
ModSources/lpr_anim_quaternius/Module/Animations/catalogue.json
Normal file
File diff suppressed because it is too large
Load diff
461
Tools/anim/Catalogue.py
Normal file
461
Tools/anim/Catalogue.py
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Catalogue des animations d'un mod (lot AP6) : catalogue.json, catalogue.html, vignettes.
|
||||
|
||||
Entrees : pack.json (declare), livraison.json + fiches_calculees.json + rapport.json + csv de qualite (calcule),
|
||||
vignettes Saved/Measurement/vignettes/<Clip>.png (carres 512 px), planches Saved/Measurement/planches/<Clip>.jpg + .json
|
||||
(bande de N images carrees de 256 px, animee au survol), clips du mod ModSources/<mod>/Content/Anims.
|
||||
Controles bloquants : code de sortie 1 et liste des causes ; rien n'est ecrit dans --out en cas d'echec.
|
||||
Python 3 standard, sans dependance.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import datetime
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
BASE_KEYS = ["martialart", "sneak", "dance", "smoke", "write", "drinkbottle", "drinkglass", "drinktea",
|
||||
"camera", "call", "browse", "makeup", "eat", "sweep", "fightlost", "gun", "knockedout"]
|
||||
TRIAL_MODULE = "lpr_anim_essai"
|
||||
SENS_FR = {"sur place": "sur place", "avant": "avance", "arriere": "recule", "cote": "de côté"}
|
||||
|
||||
|
||||
def read_json(path):
|
||||
with open(path, encoding="utf-8-sig") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def sha256_of(paths):
|
||||
h = hashlib.sha256()
|
||||
for p in paths:
|
||||
with open(p, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1 << 20), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest().upper()
|
||||
|
||||
|
||||
def read_csv_rows(path):
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
with open(path, encoding="utf-8-sig", newline="") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def num(v):
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def game_keys(root):
|
||||
"""Cles deja utilisees par le jeu (hors module d'essai) : ANIMATION: <cle> et animate(<cle>)."""
|
||||
keys = set()
|
||||
mods = os.path.join(root, "Content", "Modules")
|
||||
pat = re.compile(r"ANIMATION:\s*([A-Za-z0-9_]+)|animate\(\s*([A-Za-z0-9_]+)\s*\)")
|
||||
for dp, dn, fn in os.walk(mods):
|
||||
rel = os.path.relpath(dp, mods).replace("\\", "/")
|
||||
if rel.split("/")[0] == TRIAL_MODULE:
|
||||
dn[:] = []
|
||||
continue
|
||||
for name in fn:
|
||||
if name.lower().endswith((".lpaction", ".lpscene")):
|
||||
try:
|
||||
with open(os.path.join(dp, name), encoding="utf-8-sig", errors="replace") as f:
|
||||
text = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
for m in pat.finditer(text):
|
||||
keys.add((m.group(1) or m.group(2)).lower())
|
||||
return keys
|
||||
|
||||
|
||||
def other_catalogue_keys(root, mod, out_dir):
|
||||
keys = {}
|
||||
base = os.path.join(root, "ModSources")
|
||||
if not os.path.isdir(base):
|
||||
return keys
|
||||
own = os.path.normcase(os.path.abspath(os.path.join(out_dir, "catalogue.json")))
|
||||
for d in sorted(os.listdir(base)):
|
||||
p = os.path.join(base, d, "Module", "Animations", "catalogue.json")
|
||||
if d == mod or not os.path.isfile(p) or os.path.normcase(os.path.abspath(p)) == own:
|
||||
continue
|
||||
try:
|
||||
for a in read_json(p).get("animations", []):
|
||||
keys[str(a.get("cle", "")).lower()] = d
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
return keys
|
||||
|
||||
|
||||
def build(args, errors):
|
||||
root = os.path.abspath(args.root)
|
||||
pipe = os.path.abspath(args.pipeline)
|
||||
pack_path = os.path.abspath(args.pack)
|
||||
out = os.path.abspath(args.out)
|
||||
mod_dir = os.path.abspath(args.mod_dir) if args.mod_dir else os.path.join(root, "ModSources", args.mod)
|
||||
vign = os.path.abspath(args.vignettes) if args.vignettes else os.path.join(root, "Saved", "Measurement", "vignettes")
|
||||
plan = os.path.abspath(args.planches) if args.planches else os.path.join(root, "Saved", "Measurement", "planches")
|
||||
|
||||
def load(path, what):
|
||||
if not os.path.isfile(path):
|
||||
errors.append("%s introuvable : %s" % (what, path))
|
||||
return None
|
||||
try:
|
||||
return read_json(path)
|
||||
except ValueError as e:
|
||||
errors.append("%s illisible : %s (%s)" % (what, path, e))
|
||||
return None
|
||||
|
||||
pack = load(pack_path, "pack.json")
|
||||
liv_path = os.path.join(pipe, "livraison.json")
|
||||
fic_path = os.path.join(pipe, "fiches_calculees.json")
|
||||
liv = load(liv_path, "livraison.json")
|
||||
fic = load(fic_path, "fiches_calculees.json")
|
||||
rap = load(os.path.join(pipe, "rapport.json"), "rapport.json")
|
||||
if None in (pack, liv, fic, rap):
|
||||
return None
|
||||
|
||||
# Rapport et fiches calculees.
|
||||
if rap.get("valide") is not True:
|
||||
errors.append("rapport.json non valide : %s" % "; ".join(map(str, rap.get("echecs") or [])))
|
||||
if fic.get("echecs"):
|
||||
errors.append("fiches_calculees.json contient des echecs : %s" % "; ".join(map(str, fic["echecs"])))
|
||||
if os.path.getmtime(fic_path) <= os.path.getmtime(liv_path):
|
||||
errors.append("fiches_calculees.json (%s) n'est pas plus recent que livraison.json (%s) : "
|
||||
"le catalogue ne decrit pas les clips livres"
|
||||
% (datetime.datetime.fromtimestamp(os.path.getmtime(fic_path)).isoformat(" ", "seconds"),
|
||||
datetime.datetime.fromtimestamp(os.path.getmtime(liv_path)).isoformat(" ", "seconds")))
|
||||
fiches = fic.get("fiches", {})
|
||||
declared = pack.get("animations", {})
|
||||
|
||||
# Cles : uniques, sans collision.
|
||||
keys = [str(e.get("cle", "")) for e in liv]
|
||||
seen = set()
|
||||
for k in keys:
|
||||
if k in seen:
|
||||
errors.append("cle en double dans livraison.json : %s" % k)
|
||||
seen.add(k)
|
||||
taken = {k: "jeu de base" for k in BASE_KEYS}
|
||||
for k in game_keys(root):
|
||||
taken.setdefault(k, "scenario du jeu")
|
||||
for k, d in other_catalogue_keys(root, args.mod, out).items():
|
||||
taken.setdefault(k, "catalogue de %s" % d)
|
||||
for k in keys:
|
||||
if k.lower() in taken:
|
||||
errors.append("collision de cle : %s deja utilisee (%s)" % (k, taken[k.lower()]))
|
||||
|
||||
# Qualite.
|
||||
a_revoir = {}
|
||||
for line in rap.get("a_revoir") or []:
|
||||
parts = [p.strip() for p in str(line).split(" : ", 2)]
|
||||
if len(parts) == 3:
|
||||
a_revoir.setdefault(parts[1], []).append("%s : %s" % (parts[0], parts[2]))
|
||||
qual = {}
|
||||
for corps in ("Bridget", "Michael"):
|
||||
for r in read_csv_rows(os.path.join(pipe, "eval_%s.csv" % corps)):
|
||||
q = qual.setdefault(r["clip"], {"brut": 0.0, "torsion": 0.0})
|
||||
q["brut"] = max(q["brut"], num(r.get("brut_pire")))
|
||||
q["torsion"] = max(q["torsion"], num(r.get("torsion_deg")))
|
||||
|
||||
animations = []
|
||||
thumbs = []
|
||||
for e in liv:
|
||||
k = str(e.get("cle", ""))
|
||||
d = declared.get(k)
|
||||
f = fiches.get(k)
|
||||
if d is None:
|
||||
errors.append("%s : absente de pack.json" % k)
|
||||
if f is None:
|
||||
errors.append("%s : fiche calculee absente" % k)
|
||||
if d is None or f is None:
|
||||
continue
|
||||
clip = d.get("clip", "")
|
||||
miss = []
|
||||
for name, val in (("clip", clip), ("nom.fr", (d.get("nom") or {}).get("fr")), ("nom.en", (d.get("nom") or {}).get("en")),
|
||||
("categorie", d.get("categorie")), ("style", d.get("style")),
|
||||
("usage.fr", (d.get("usage") or {}).get("fr")), ("usage.en", (d.get("usage") or {}).get("en"))):
|
||||
if not val:
|
||||
miss.append(name)
|
||||
for name in ("images", "cadence", "duree_s", "boucle", "deplacement_cm", "vitesse_cm_s", "deplacement_sens",
|
||||
"posture", "pieds_au_sol_pct", "mains"):
|
||||
if f.get(name) in (None, "", {}):
|
||||
miss.append("fiche." + name)
|
||||
for name in ("source", "licence", "credit"):
|
||||
if not pack.get({"source": "source", "licence": "licence", "credit": "credit"}[name]):
|
||||
miss.append("pack." + name)
|
||||
for name in ("asset_F", "asset_M"):
|
||||
if not e.get(name):
|
||||
miss.append("livraison." + name)
|
||||
if miss:
|
||||
errors.append("%s : champs obligatoires vides : %s" % (k, ", ".join(miss)))
|
||||
thumb = os.path.join(vign, "%s.png" % clip)
|
||||
if not clip or not os.path.isfile(thumb):
|
||||
errors.append("%s : vignette absente (%s)" % (k, thumb))
|
||||
pj = os.path.join(plan, "%s.jpg" % clip)
|
||||
pm = os.path.join(plan, "%s.json" % clip)
|
||||
pinfo = None
|
||||
if not clip or not os.path.isfile(pj):
|
||||
errors.append("%s : planche absente (%s)" % (k, pj))
|
||||
elif not os.path.isfile(pm):
|
||||
errors.append("%s : description de planche absente (%s)" % (k, pm))
|
||||
else:
|
||||
try:
|
||||
pinfo = read_json(pm)
|
||||
n_img = pinfo["images"]
|
||||
if isinstance(n_img, bool) or not isinstance(n_img, int) or n_img < 2:
|
||||
errors.append("%s : planche invalide, images = %r (au moins 2 attendues) : %s" % (k, n_img, pm))
|
||||
pinfo = None
|
||||
elif pinfo.get("taille") != 256:
|
||||
errors.append("%s : planche invalide, taille = %r (256 attendu) : %s" % (k, pinfo.get("taille"), pm))
|
||||
pinfo = None
|
||||
except (ValueError, KeyError, TypeError, AttributeError) as e:
|
||||
errors.append("%s : description de planche illisible : %s (%r)" % (k, pm, e))
|
||||
pinfo = None
|
||||
files = [os.path.join(mod_dir, "Content", "Anims", "Female", k + "_F.uasset"),
|
||||
os.path.join(mod_dir, "Content", "Anims", "Male", k + "_M.uasset")]
|
||||
for p in files:
|
||||
if not os.path.isfile(p):
|
||||
errors.append("%s : clip du mod absent : %s" % (k, p))
|
||||
if miss or not os.path.isfile(files[0]) or not os.path.isfile(files[1]) or not os.path.isfile(thumb) or pinfo is None:
|
||||
continue # causes deja listees ci-dessus
|
||||
|
||||
tf, tm = os.path.getsize(files[0]), os.path.getsize(files[1])
|
||||
motifs = a_revoir.get(clip, [])
|
||||
q = qual.get(clip, {"brut": 0.0, "torsion": 0.0})
|
||||
decl = d.get("posture")
|
||||
if decl is not None and (not isinstance(decl, str) or not decl.strip()):
|
||||
errors.append("%s : posture declaree invalide dans pack.json" % k)
|
||||
continue
|
||||
posture = {"principale": decl if decl else f["posture"],
|
||||
"posture_origine": "declaree" if decl else "calculee"}
|
||||
if decl:
|
||||
posture["posture_calculee"] = f["posture"]
|
||||
if f.get("posture_debut") != f.get("posture_fin"):
|
||||
posture["debut"], posture["fin"] = f.get("posture_debut"), f.get("posture_fin")
|
||||
# Hauteur d'assise seulement pour une posture assise (une chute traverse des images "assis").
|
||||
if f.get("hauteur_assise_cm") is not None and posture["principale"] == "assis":
|
||||
posture["hauteur_assise_cm"] = f["hauteur_assise_cm"]
|
||||
dur_p = float(f["duree_s"]) or num(pinfo.get("duree_s"))
|
||||
if dur_p <= 0:
|
||||
errors.append("%s : duree nulle, cadence de planche incalculable" % k)
|
||||
continue
|
||||
animations.append({
|
||||
"cle": k,
|
||||
"appel": {"script": "Actor.animate(%s)" % k, "action": "ANIMATION: %s" % k},
|
||||
"nom": {"fr": d["nom"]["fr"], "en": d["nom"]["en"]},
|
||||
"assets": {"feminin": e["asset_F"], "masculin": e["asset_M"]},
|
||||
"duree_s": round(float(f["duree_s"]), 2),
|
||||
"images": f["images"],
|
||||
"planche": {"fichier": "planches/%s.jpg" % k, "images": pinfo["images"], "taille": 256,
|
||||
"ips": round(pinfo["images"] / dur_p, 1)},
|
||||
"cadence": f["cadence"],
|
||||
"taille_ko": {"feminin": round(tf / 1024), "masculin": round(tm / 1024), "total": round((tf + tm) / 1024)},
|
||||
"taille_cuite": False,
|
||||
"boucle": {"oui": bool(f["boucle"]), "ecart_pct": round(f.get("boucle_ecart_pct") or 0.0, 2)},
|
||||
"deplacement": {"cm": f["deplacement_cm"], "cm_s": f["vitesse_cm_s"], "sens": f["deplacement_sens"]},
|
||||
"posture": posture,
|
||||
"contacts": {"pieds_au_sol_pct": f["pieds_au_sol_pct"], "mains": f["mains"]},
|
||||
"accessoire": d.get("accessoire", ""),
|
||||
"categorie": d["categorie"],
|
||||
"style": d["style"],
|
||||
"usage": {"fr": d["usage"]["fr"], "en": d["usage"]["en"]},
|
||||
"corps": ["feminin", "masculin"],
|
||||
"qualite": {"verdict": "à revoir" if motifs else "accepté", "motifs": motifs,
|
||||
"pire_brut_pct": round(q["brut"], 2), "pire_torsion_deg": round(q["torsion"], 2)},
|
||||
"source": pack.get("source", ""),
|
||||
"licence": pack.get("licence", ""),
|
||||
"credit": pack.get("credit", ""),
|
||||
"version": args.version,
|
||||
"empreinte": sha256_of(files),
|
||||
})
|
||||
thumbs.append((k, thumb, pj))
|
||||
if errors:
|
||||
return None
|
||||
header = {
|
||||
"pack": pack.get("pack", ""), "mod": args.mod, "version": args.version,
|
||||
"genere": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"nombre_animations": len(animations), "credit": pack.get("credit", ""),
|
||||
"animations": animations,
|
||||
}
|
||||
return header, thumbs, out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- HTML
|
||||
|
||||
CSS = """
|
||||
:root{--bg:#f6f7f9;--fg:#1d2330;--mut:#5d6677;--card:#fff;--bd:#d9dde5;--acc:#2057c9;--ok:#1d7a3f;--warn:#a85b00;--chip:#eef1f6}
|
||||
@media (prefers-color-scheme:dark){:root{--bg:#14171d;--fg:#e6e9ef;--mut:#9aa3b4;--card:#1d222b;--bd:#323a48;--acc:#7aa7ff;--ok:#5fcf8a;--warn:#f0a850;--chip:#262c37}}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.45 system-ui,Segoe UI,sans-serif;overflow-wrap:anywhere}
|
||||
header{padding:16px;max-width:1200px;margin:0 auto}
|
||||
h1{margin:0 0 4px;font-size:1.4rem}
|
||||
.sub{color:var(--mut);font-size:.9rem}
|
||||
.bar{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
|
||||
.bar input,.bar select{font:inherit;padding:6px 8px;border:1px solid var(--bd);border-radius:6px;background:var(--card);color:var(--fg);min-width:0;max-width:100%}
|
||||
.bar input{flex:1 1 220px}
|
||||
main{max-width:1200px;margin:0 auto;padding:0 16px 24px;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));gap:12px}
|
||||
.card{background:var(--card);border:1px solid var(--bd);border-radius:10px;overflow:hidden;display:flex;flex-direction:column}
|
||||
.card[hidden],.card img[hidden],.card .anim[hidden]{display:none}
|
||||
.card img{width:100%;aspect-ratio:1/1;object-fit:contain;background:var(--chip);display:block}
|
||||
.card .anim{width:100%;aspect-ratio:1/1;background-color:var(--chip);background-repeat:no-repeat}
|
||||
.in{padding:10px 12px;display:flex;flex-direction:column;gap:6px}
|
||||
.in h2{margin:0;font-size:1.05rem}
|
||||
.key{font-family:Consolas,monospace;background:var(--chip);border:1px solid var(--bd);border-radius:6px;padding:3px 6px;cursor:pointer;text-align:left;color:var(--fg);font-size:.85rem}
|
||||
.call{font-family:Consolas,monospace;font-size:.8rem;color:var(--mut)}
|
||||
.tags{display:flex;flex-wrap:wrap;gap:4px}
|
||||
.tags span{background:var(--chip);border-radius:10px;padding:1px 8px;font-size:.78rem}
|
||||
.use{font-size:.88rem}
|
||||
.q-ok{color:var(--ok)}.q-rev{color:var(--warn);font-weight:600}
|
||||
footer{max-width:1200px;margin:0 auto;padding:0 16px 24px;color:var(--mut);font-size:.8rem}
|
||||
#none{display:none;padding:16px;max-width:1200px;margin:0 auto}
|
||||
"""
|
||||
|
||||
JS = """
|
||||
const A=JSON.parse(document.getElementById('data').textContent).animations;
|
||||
const $=id=>document.getElementById(id);
|
||||
const cap=s=>s.charAt(0).toUpperCase()+s.slice(1);
|
||||
function dur(a){return a.duree_s<2?'courte':(a.duree_s>5?'longue':'moyenne')}
|
||||
function fill(sel,vals){[...new Set(vals)].sort().forEach(v=>{const o=document.createElement('option');o.value=v;o.textContent=v;sel.appendChild(o)})}
|
||||
fill($('fcat'),A.map(a=>a.categorie));fill($('fpos'),A.map(a=>a.posture.principale));
|
||||
const SENS={'sur place':'sur place','avant':'avance','arriere':'recule','cote':'de côté'};
|
||||
function esc(s){return String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]))}
|
||||
function card(a){
|
||||
const dp=a.deplacement,p=a.posture;
|
||||
let pos=p.principale;if(p.debut)pos+=' ('+p.debut+' → '+p.fin+')';if(p.hauteur_assise_cm!=null)pos+=', assise '+p.hauteur_assise_cm+' cm';
|
||||
const mv=dp.sens==='sur place'?'sur place':(SENS[dp.sens]||dp.sens)+' '+Math.round(dp.cm)+' cm ('+Math.round(dp.cm_s)+' cm/s)';
|
||||
const q=a.qualite,qc=q.verdict==='accepté'?'q-ok':'q-rev';
|
||||
const el=document.createElement('article');el.className='card';
|
||||
el.innerHTML='<img loading="lazy" src="vignettes/'+esc(a.cle)+'.png" alt="'+esc(a.nom.fr)+'">'
|
||||
+'<div class="in"><h2>'+esc(a.nom.fr)+' <span class="sub">/ '+esc(a.nom.en)+'</span></h2>'
|
||||
+'<button class="key" type="button" title="Copier la clé">'+esc(a.cle)+'</button>'
|
||||
+'<div class="call">'+esc(a.appel.script)+'<br>'+esc(a.appel.action)+'</div>'
|
||||
+'<div class="tags"><span>'+esc(a.categorie)+'</span><span>'+esc(pos)+'</span><span>'+a.duree_s.toString().replace('.',',')+' s ('+dur(a)+')</span>'
|
||||
+'<span>'+(a.boucle.oui?'boucle':'une fois')+'</span><span>'+esc(mv)+'</span>'+(a.accessoire?'<span>'+esc(a.accessoire)+'</span>':'')+'</div>'
|
||||
+'<div class="use">'+esc(a.usage.fr)+'</div>'
|
||||
+'<div class="'+qc+'">Qualité : '+esc(q.verdict)+(q.motifs.length?' ('+esc(q.motifs.join(' ; '))+')':'')+'</div></div>';
|
||||
hover(el,a);
|
||||
const b=el.querySelector('.key');
|
||||
b.onclick=()=>{const done=()=>{b.textContent='copié';setTimeout(()=>b.textContent=a.cle,900)};
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(a.cle).then(done,()=>sel(b))}else sel(b)};
|
||||
return el}
|
||||
const reduce=matchMedia('(prefers-reduced-motion: reduce)');
|
||||
function hover(el,a){
|
||||
const pl=a.planche,img=el.querySelector('img');let div=null,timer=null,ready=false,want=false,last='mouse';
|
||||
function show(){if(!want||!ready)return;img.hidden=true;div.hidden=false;let i=0;div.style.backgroundPosition='0% 0%';
|
||||
clearInterval(timer);timer=setInterval(()=>{i=(i+1)%pl.images;div.style.backgroundPosition=(i*100/(pl.images-1))+'% 0%'},1000/pl.ips)}
|
||||
function start(){want=true;
|
||||
if(!div){div=document.createElement('div');div.className='anim';div.hidden=true;div.setAttribute('role','img');div.setAttribute('aria-label',a.nom.fr);
|
||||
div.style.backgroundSize=(pl.images*100)+'% 100%';img.parentNode.insertBefore(div,img);
|
||||
const im=new Image();im.onload=()=>{div.style.backgroundImage='url("'+pl.fichier+'")';ready=true;show()};im.src=pl.fichier}
|
||||
else show()}
|
||||
function stop(){want=false;clearInterval(timer);timer=null;if(div)div.hidden=true;img.hidden=false}
|
||||
el.addEventListener('pointerenter',e=>{if(e.pointerType!=='touch'&&!reduce.matches)start()});
|
||||
el.addEventListener('pointerleave',e=>{if(e.pointerType!=='touch')stop()});
|
||||
el.addEventListener('pointerdown',e=>{last=e.pointerType});
|
||||
el.addEventListener('click',e=>{if(!e.target.closest('img,.anim'))return;if(last==='touch'||reduce.matches){want?stop():start()}})}
|
||||
function sel(b){const r=document.createRange();r.selectNodeContents(b);const s=getSelection();s.removeAllRanges();s.addRange(r)}
|
||||
const main=$('list'),cards=A.map(a=>({a,el:card(a)}));cards.forEach(c=>main.appendChild(c.el));
|
||||
function apply(){
|
||||
const t=$('q').value.trim().toLowerCase(),c=$('fcat').value,p=$('fpos').value,d=$('fdur').value;let n=0;
|
||||
cards.forEach(x=>{const a=x.a;
|
||||
const hay=(a.cle+' '+a.nom.fr+' '+a.nom.en+' '+a.usage.fr+' '+a.usage.en+' '+a.accessoire+' '+a.style).toLowerCase();
|
||||
const ok=(!t||hay.includes(t))&&(!c||a.categorie===c)&&(!p||a.posture.principale===p)&&(!d||dur(a)===d);
|
||||
x.el.hidden=!ok;if(ok)n++});
|
||||
$('count').textContent=n+' / '+A.length+' animation(s)';$('none').style.display=n?'none':'block'}
|
||||
['q','fcat','fpos','fdur'].forEach(i=>$(i).addEventListener('input',apply));apply();
|
||||
"""
|
||||
|
||||
|
||||
def render_html(cat):
|
||||
data = json.dumps(cat, ensure_ascii=False).replace("</", "<\\/")
|
||||
title = "Animations — %s" % cat["mod"]
|
||||
return """<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>%(title)s</title>
|
||||
<style>%(css)s</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>%(title)s</h1>
|
||||
<div class="sub">Version %(version)s — %(n)d animations — généré le %(date)s. Copiez la clé puis écrivez <code>Actor.animate(clé)</code> dans une scène ou <code>ANIMATION: clé</code> dans une action. Survolez une vignette pour voir le mouvement.</div>
|
||||
<div class="bar">
|
||||
<input id="q" type="search" placeholder="Rechercher (nom, clé, usage…)" aria-label="Recherche">
|
||||
<select id="fcat" aria-label="Catégorie"><option value="">Toutes catégories</option></select>
|
||||
<select id="fpos" aria-label="Posture"><option value="">Toutes postures</option></select>
|
||||
<select id="fdur" aria-label="Durée"><option value="">Toutes durées</option><option value="courte">Courte (< 2 s)</option><option value="moyenne">Moyenne (2 à 5 s)</option><option value="longue">Longue (> 5 s)</option></select>
|
||||
</div>
|
||||
<div id="count" class="sub" style="margin-top:8px"></div>
|
||||
</header>
|
||||
<div id="none">Aucune animation ne correspond aux filtres.</div>
|
||||
<main id="list"></main>
|
||||
<footer>%(credit)s</footer>
|
||||
<script id="data" type="application/json">%(data)s</script>
|
||||
<script>%(js)s</script>
|
||||
</body>
|
||||
</html>
|
||||
""" % {"title": html.escape(title), "css": CSS, "js": JS, "data": data, "version": html.escape(cat["version"]),
|
||||
"n": cat["nombre_animations"], "date": html.escape(cat["genere"][:10]), "credit": html.escape(cat["credit"])}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--pack", required=True)
|
||||
ap.add_argument("--pipeline", required=True)
|
||||
ap.add_argument("--mod", required=True)
|
||||
ap.add_argument("--version", required=True)
|
||||
ap.add_argument("--out", required=True)
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
ap.add_argument("--root", default=os.path.dirname(os.path.dirname(here)), help="racine du projet")
|
||||
ap.add_argument("--mod-dir", default="", help="dossier du mod (defaut : <root>/ModSources/<mod>)")
|
||||
ap.add_argument("--planches", default="", help="dossier des planches <Clip>.jpg/.json (defaut : <root>/Saved/Measurement/planches)")
|
||||
ap.add_argument("--vignettes", default="", help="dossier des vignettes <Clip>.png (defaut : <root>/Saved/Measurement/vignettes)")
|
||||
args = ap.parse_args()
|
||||
|
||||
errors = []
|
||||
result = None
|
||||
try:
|
||||
result = build(args, errors)
|
||||
except Exception as e: # noqa: BLE001
|
||||
errors.append("erreur inattendue : %r" % (e,))
|
||||
rapport = os.path.join(os.path.abspath(args.pipeline), "catalogue_rapport.json")
|
||||
info = {"date": datetime.datetime.now().isoformat(timespec="seconds"), "mod": args.mod, "version": args.version,
|
||||
"ok": result is not None and not errors, "fiches": len(result[0]["animations"]) if result else 0,
|
||||
"echecs": errors}
|
||||
if result is None or errors:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(rapport), exist_ok=True)
|
||||
with open(rapport, "w", encoding="utf-8", newline="\n") as f:
|
||||
json.dump(info, f, ensure_ascii=False, indent=2)
|
||||
except OSError:
|
||||
pass
|
||||
print("CATALOGUE ECHEC : %d cause(s)" % len(errors), file=sys.stderr)
|
||||
for e in errors:
|
||||
print(" - " + e, file=sys.stderr)
|
||||
return 1
|
||||
cat, thumbs, out = result
|
||||
os.makedirs(os.path.join(out, "vignettes"), exist_ok=True)
|
||||
os.makedirs(os.path.join(out, "planches"), exist_ok=True)
|
||||
for k, src, psrc in thumbs:
|
||||
shutil.copyfile(src, os.path.join(out, "vignettes", k + ".png"))
|
||||
shutil.copyfile(psrc, os.path.join(out, "planches", k + ".jpg"))
|
||||
with open(os.path.join(out, "catalogue.json"), "w", encoding="utf-8", newline="\n") as f:
|
||||
json.dump(cat, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
with open(os.path.join(out, "catalogue.html"), "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(render_html(cat))
|
||||
with open(rapport, "w", encoding="utf-8", newline="\n") as f:
|
||||
json.dump(info, f, ensure_ascii=False, indent=2)
|
||||
print("CATALOGUE OK : %d fiche(s) -> %s" % (len(cat["animations"]), out))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -13,6 +13,9 @@
|
|||
dans UnrealEditor-Cmd (journal Saved\Logs\portage\AP5_DeliverAnims_<horodatage>.log), remet le mod en place,
|
||||
controle la sortie (LIVRAISON copies 2n erreurs 0, n fichiers .uasset par sexe). Code de sortie 1 au moindre ecart.
|
||||
|
||||
-Catalogue : apres une livraison reussie, lance Tools\anim\Catalogue.py (lot AP6).
|
||||
-CatalogueOnly : saute la copie et ne produit que le catalogue (rapport valide exige), sans lancer Unreal.
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\anim\DeliverAnims.ps1 -DryRun
|
||||
.\Tools\anim\DeliverAnims.ps1
|
||||
|
|
@ -23,7 +26,9 @@ param(
|
|||
[string] $Prefix = 'qua',
|
||||
[string] $Exclude = 'A_TPose',
|
||||
[string] $Report = '',
|
||||
[switch] $DryRun
|
||||
[switch] $DryRun,
|
||||
[switch] $Catalogue,
|
||||
[switch] $CatalogueOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
|
@ -39,6 +44,21 @@ function New-IfMissing([string] $Path, [string] $Text) {
|
|||
Write-Host " cree : $Path"
|
||||
}
|
||||
|
||||
# Lot AP6 : catalogue du mod (Tools\anim\Catalogue.py), version lue dans le .lpmod.
|
||||
function Invoke-Catalogue {
|
||||
$lpmodPath = Join-Path $Root "ModSources\$Name\Module\$Name.lpmod"
|
||||
if (-not (Test-Path $lpmodPath)) { throw "lpmod introuvable : $lpmodPath" }
|
||||
$line = Select-String -Path $lpmodPath -Pattern '^MODULE_VERSION:\s*(\S+)' | Select-Object -First 1
|
||||
if (-not $line) { throw "MODULE_VERSION absente de $lpmodPath" }
|
||||
$version = $line.Matches[0].Groups[1].Value
|
||||
$pipeline = Split-Path $Report -Parent
|
||||
$out = Join-Path $Root "ModSources\$Name\Module\Animations"
|
||||
Write-Host "Catalogue : version $version -> $out"
|
||||
& python (Join-Path $Root 'Tools\anim\Catalogue.py') --pack (Join-Path $Root 'AnimSources\quaternius\pack.json') `
|
||||
--pipeline $pipeline --mod $Name --version $version --out $out
|
||||
if ($LASTEXITCODE -ne 0) { throw "Catalogue.py a echoue (code $LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
try {
|
||||
# 1. Verdict des controles qualite.
|
||||
if (-not (Test-Path $Report)) { throw "rapport.json introuvable : $Report" }
|
||||
|
|
@ -49,6 +69,11 @@ try {
|
|||
}
|
||||
foreach ($r in @($rapport.a_revoir)) { Write-Warning "a revoir : $r" }
|
||||
|
||||
if ($CatalogueOnly -and -not $DryRun) {
|
||||
Invoke-Catalogue
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 2. Controles d'environnement.
|
||||
$running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue)
|
||||
if ($running) { throw "UnrealEditor est ouvert (pid $($running[0].Id)) : le fermer avant la livraison" }
|
||||
|
|
@ -140,6 +165,7 @@ try {
|
|||
if ($count -ne $n) { throw "$dir : $count fichier(s) .uasset pour $n cle(s)" }
|
||||
}
|
||||
Write-Host "Livraison terminee : $n cle(s), $($n * 2) copies"
|
||||
if ($Catalogue) { Invoke-Catalogue }
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
|
|
|
|||
Loading…
Reference in a new issue