Divine Skins
Divine Skins Wiki

Persistent Effect Conditions

Changer de submeshes et de VFX selon l'état du jeu : buffs, animations en cours, délais temporisés. Le mécanisme derrière les changements de forme, les signaux de passif prêt et les skins de transformation.

La plupart des VFX d'un skin sont déclenchés par un event d'animation : le clip atteint la frame 20 et un effet apparaît. Les persistent effect conditions font l'inverse. Elles surveillent un morceau d'état du jeu et gardent quelque chose allumé tant que cet état tient.

C'est comme ça qu'un champion change de modèle quand son ultime est actif, qu'un glow de passif prêt apparaît et disparaît tout seul, et qu'un visage passe en expression de rage dès qu'un buff tombe.


Outils requis

OutilRôle
FlintExtraire le champion pour récupérer le bin du skin
JadeÉditer le .bin du skin visuellement (recommandé)
RitobinAlternative : convertir le .bin en .py pour l'éditer

Où ça se trouve

PersistentEffectConditions est une liste sur SkinCharacterDataProperties, dans le bin du skin, écrite juste après mResourceResolver. Chaque entrée de la liste, c'est une condition et ce qu'elle allume ou éteint.

Le même champ existe aussi sur CharacterRecord dans le bin du champion, où Riot en met parfois un pour qu'il s'applique à tous les skins. Modifie la copie du bin du skin, pas celle-là, comme ça tu n'as pas à livrer le bin du champion.


Ce qu'elle peut changer, et ce qu'elle ne peut pas

Chaque PersistentEffectConditionData porte une condition et jusqu'à trois payloads :

ChampCe qu'il change
SubmeshesToShowLes submeshes rendus visibles tant que la condition tient
SubmeshesToHideLes submeshes cachés tant qu'elle tient
PersistentVfxsLes effets créés et maintenus en vie tant qu'elle tient

Elle ne peut pas changer une animation. Il n'y a aucun champ de clip dans cette structure, et le chercher est un bon moyen de perdre un après-midi. Les clips d'animation se changent à part, dans le bin d'animation, avec ConditionBoolClipData et ConditionFloatClipData.

Les deux systèmes partagent une grande partie du même vocabulaire de drivers, d'où l'impression que c'est la même chose. Ça ne l'est pas. Un vrai changement de forme demande d'écrire les deux : une persistent effect condition pour le modèle et les effets, et un condition clip pour le set d'animations, chacun avec sa propre copie de la condition.


L'exemple le plus simple possible

Un glow présent par défaut, qui disparaît pendant que le passif est en cooldown :

PersistentEffectConditions: list2[pointer] = {
    PersistentEffectConditionData {
        OwnerCondition: pointer = NotMaterialDriver {
            mDriver: pointer = HasBuffDynamicMaterialBoolDriver {
                Spell: hash = "Characters/Poppy/Spells/PoppyPassiveAbility/PoppyPassiveCooldown" } }
        PersistentVfxs: list2[embed] = {
            PersistentVfxData {
                EffectKey: hash = "Slayer_Passive_Ready"
                BoneName: string = "Buffbone_Shield" } } } }

Remarque l'inversion. Au lieu de te brancher sur le buff qui s'active et se désactive, tu testes le buff de cooldown et tu le nies. L'effet est allumé chaque fois que le cooldown ne l'est pas.


Les drivers

OwnerCondition accepte n'importe lequel d'entre eux. Il accepte un driver nu, donc le wrapper AllTrue est facultatif quand tu n'as qu'une seule condition.

HasBuffDynamicMaterialBoolDriver demande si un buff est actif sur le champion. Deux formes existent et les deux marchent :

HasBuffDynamicMaterialBoolDriver { Spell: hash = "Characters/Nasus/Spells/NasusRAbility/NasusR" }
HasBuffDynamicMaterialBoolDriver { mScriptName: string = "UndyingRage" }

La première pointe vers un chemin de sort, la seconde nomme directement un script de buff.

IsAnimationPlayingDynamicMaterialBoolDriver contient une liste de noms de clips et vaut vrai tant que l'un d'eux se joue :

IsAnimationPlayingDynamicMaterialBoolDriver {
    mAnimationNames: list[hash] = { "Spell2" "Spell2_0" "Spell2_90" "Spell2_-90"
                                    "Spell2_180" "Spell2_-180" "Taunt" } }

Note : La liste est un OU, et il te faut en général toutes les variantes. Si le graphe d'animation choisit un clip lié à une direction, ne lister que le nom de base fait clignoter ton effet à tous les angles sauf un.

DelayedBoolMaterialDriver enveloppe un autre driver et ajoute du timing :

DelayedBoolMaterialDriver {
    mBoolDriver: pointer = IsAnimationPlayingDynamicMaterialBoolDriver {
        mAnimationNames: list[hash] = { "spell1" } }
    mDelayOn: f32 = 0
    mDelayOff: f32 = 5 }

mDelayOff, c'est le temps pendant lequel l'état persiste après que la condition est redevenue fausse. Cinq secondes ici gardent une arme visible après la fin de la compétence qui l'a invoquée.

NotMaterialDriver inverse. AllTrueMaterialDriver prend une liste et exige que tous soient vrais.

Il n'existe pas de driver OU. Exprime le OU en écrivant deux blocs de condition, ou en mettant plusieurs noms dans une seule liste d'animations.


Construire une petite machine à états

ET et NON suffisent pour des états qui s'excluent. Trois états d'yeux à partir de deux buffs :

// normal: neither ability active
OwnerCondition = AllTrueMaterialDriver { mDrivers = {
    NotMaterialDriver { mDriver = HasBuff... NasusR }
    NotMaterialDriver { mDriver = HasBuff... NasusQ } } }

// ultimate: R active, Q not
OwnerCondition = AllTrueMaterialDriver { mDrivers = {
    HasBuff... NasusR
    NotMaterialDriver { mDriver = HasBuff... NasusQ } } }

Chaque bloc pointe vers un effet différent, et comme les deux conditions ne peuvent pas être vraies en même temps, tu obtiens une bascule propre au lieu de deux effets qui se battent.


Les changements de forme

Une seule condition avec plusieurs payloads dans le même bloc, c'est la brique de base d'une transformation :

PersistentEffectConditionData {
    OwnerCondition: pointer = AllTrueMaterialDriver { mDrivers: list[pointer] = {
        HasBuffDynamicMaterialBoolDriver { mScriptName: string = "UndyingRage" } } }
    SubmeshesToShow: list2[hash] = { "Face_Rage"  "Sword"  "Metal" }
    SubmeshesToHide: list2[hash] = { "Face_Neutral"  "Face_Happy"  "Face_Laugh"
                                     "Face_Shout"  "Face_Smirk" } }

Là, c'est tout un jeu d'expressions faciales échangé d'un coup quand un buff tombe. Ajoute un second bloc sur la même condition avec PersistentVfxs et tu as aussi les effets.

Les deux familles de payloads sont valides dans un seul bloc. Les séparer dans plusieurs blocs avec une condition identique marche aussi, et c'est ce que la plupart des gens font.


Attacher à la caméra plutôt qu'à un bone

Omets BoneName et l'effet peut être lié à la caméra à la place. C'est comme ça qu'on fait les filtres plein écran pendant une transformation :

PersistentEffectConditionData {
    OwnerCondition: pointer = AllTrueMaterialDriver { mDrivers = { HasBuff... } }
    ForceRenderVfx: bool = true
    PersistentVfxs: list2[embed] = {
        PersistentVfxData {
            effectKey: hash = "Yone_Skin58_E_CameraBoundVFX"
            ShowToOwnerOnly: bool = true
            AttachToCamera: bool = true } } }

ShowToOwnerOnly l'empêche d'apparaître sur l'écran des autres joueurs, ce que tu veux presque toujours pour un overlay d'écran.


L'effect key doit se résoudre

effectKey n'est pas un chemin de particule. C'est une clé dans mResourceResolver, qui associe cette clé à une entrée VfxSystemDefinitionData. Si la clé n'est pas dans le resolver, rien ne se joue et aucune erreur n'apparaît. C'est de loin la raison la plus fréquente pour laquelle un bloc de condition qui a l'air correct ne fait rien.

Deux façons de la brancher :

  • Donner un nom propre à l'effet. L'entrée du resolver associe un nom que tu as inventé à ton propre système. C'est le plus propre quand l'effet est le tien.
  • Rediriger une clé existante vers un système emprunté, pour qu'une clé d'origine pointe maintenant vers la particule d'un autre champion. Pratique quand tu récupères un effet ailleurs.

Deux usages moins évidents

Masquer des submeshes par animation. Comme IsAnimationPlayingDynamicMaterialBoolDriver accepte des noms de clips, tu peux cacher des props pendant un recall, afficher un trône pendant une danse et échanger un corps pendant un respawn, le tout depuis le bin du skin. Tu fais avec des conditions ce qui demanderait sinon des events de visibilité de submesh écrits un par un dans chaque clip, et c'est bien moins lourd à maintenir.

Une traînée décalée grâce aux délais. Plusieurs blocs sur la même condition, chacun avec un DelayedBoolMaterialDriver dont le mDelayOn augmente petit à petit, font apparaître le même effet à intervalles réguliers. Tu obtiens une séquence en traînée à partir d'un seul buff, sans toucher à l'emitter.


Quand ne pas l'utiliser

Si ce que tu veux, c'est un effet à une frame précise d'un clip précis, utilise plutôt un event d'animation. Les persistent conditions servent aux états qui durent, pas aux instants.

Guide par Kaizen