Un skin est du code TypeScript. À chaque lancement de map, le jeu exécute le
skin.ts du skin choisi ; ce script place le playfield natif (position,
tailles, sens de défilement, images et teintes de chaque colonne) et construit
le HUD de la surcouche WebView (barre de progression, combo, précision…) avec
l'API hud.* des mods. Pendant la partie, ses gestionnaires d'événements
changent le playfield (playfield.update, lanes.set). Le rendu natif dessine
le playfield et, avec la permission stage, les éléments natifs du skin
(voir Dessin natif) ; le HUD est entièrement dans la
surcouche.
Où sont les skins
- Skins du jeu : de simples dossiers de
skins/livrés à côté deprism.exe(en développement, le dossierskins/du dépôt, trouvé en remontant depuis l'exécutable jusqu'au dossier deCargo.lock), chargés depuis le disque exactement comme les skins du joueur : aucun skin n'est incorporé au binaire, chacun se lit, se modifie, s'exporte et se supprime comme les autres (pas de « Copier pour modifier »). Le moteur n'en connaît aucun. Les skins du jeu :default(« Prism Default », réglage par défaut) :skins/default/, le skin osu! du joueur « PRISM // arrow (cap ends) » (par april,assets/skins/default.osk) converti depuis sonskin.inipour les dispositions 4K à 7K du jeu. Colonnes de 62 px (4K ; 58, 54 et 50 px en 5K, 6K et 7K) sur un écran virtuel de 480 (laneWidth0,129 en 4K, sans écart), colonnes#07090f96, bord clair d'un pixel (les imagesStageLeftetStageRight), flèches sombres (left,down,up,right, pluscenter,upleftetuprightdes dispositions impaires ou en 6K) et leurs touches allumées à l'appui, corps de hold et embout à capuchon.HitPosition430 place le bas des notes sur la ligne : les notes sont centrées une demi-colonne plus haut (receptorY= 430/480 −laneWidth/2) et les images de touche portent des lignes transparentes au-dessus de la flèche pour que leur centre soit celui de la flèche. Combo (ComboPosition240) et dernier jugement (ScorePosition188) sont centrés. Les images@2xde l'archive sont reprises telles quelles (touches de 4K,centerde 5K etupleft/uprightde 6K ramenés à 198 px de large, les autres dispositions réutilisent les flèches de 4K) ; l'éclairage est repris (StageLight:stageLightteinté#aae4ff(ColourLight1) ;LightingN:hitLight, 8 imagesfx/explosion-{n}.png, jouée à chaque frappe ;LightingL:holdLight, 6 imagesfx/holdlight-{n}.png, àLightFramePerSecond60, sur la position de frappeLightPosition430, une demi-colonne sous la ligne des récepteurs). Ne sont pas dans le jeu : les images de jugement, de score et de combo (le jugement est un mod), les lignes de colonne (5 % de blanc), les sons, le curseur et l'animation du corps de hold (une seule des 8 images est gardée). L'archive d'origine reste hors du jeu. La police du jeu estassets/font.ttf, hors du skin ; le dossier reste sous 900 Kio (test) ;default-circle(« Default Circle », par tekkito2) :skins/default-circle/, la disposition 4K du skin osu! « tekkito2 ft jb the voice tu perfume a chanel » portée depuis sonskin.ini: colonnes de 72 px sur un écran virtuel de 480 (laneWidth 0,15, sans écart), ligne de jugement à 440 (receptorY0,9167,judgementLine), colonnes noires à 200/255, notes rondes (cercle extérieur aux colonnes 0 et 3, intérieur aux autres), combo et jugement centrés sous le playfield (ComboPosition130,ScorePosition150). Le récepteur, la « stage hint » et les images d'éclairage (lightingN,lightingL,lighting,mania-stage-light) du skin osu! sont des images transparentes : rien ne s'affiche à ces endroits, comme dans osu! (aucune imagestageLight). Les notes sont centrées sur la ligne de jugement (milieu de la note,noteOffsetY0). Crédits : le corps de hold et l'embout de hold ne viennent pas du skin de tekkito2 (son embout est transparent, son corps une image grise de 138 × 40 000 px) mais du skindefault(« PRISM // arrow (cap ends) » par april) :hold_body.pngest une copie denotes/body.png(256 × 32 px) ethold_cap.pngune copie denotes/tail-cap.png(256 × 256 px, 3,7 Kio). Ce sont des copies autonomes, le skin ne référence jamais le dossier d'un autre skin. Le corps fait la largeur des notes (holdWidth0,15), l'embout la largeur du corps (holdEndSize0,15 × 0,15, sansholdEndOffset), teintes blanches. Les images lourdes de l'archive (22 Mo), les.psd, les sons, le curseur et les scores n'y sont pas : seuls 6 fichiers PNG et le script pèsent environ 30 Kio. L'archive d'origine estassets/skins/circle-default.osk(hors du jeu).
- Skins du joueur : un dossier par skin dans
<données>\skins\<id>\(%LOCALAPPDATA%\Prism\PrismNG\data\skins), ou dans le dossier donné parPRISM_SKINS_DIR(qui ne remplace que cette racine des données du joueur ; les skins du jeu restent lus dansskins\à côté deprism.exe). La liste prend d'abord les skins du jeu, puis ceux du joueur ; si deux dossiers portent le même identifiant, le premier gagne et un message le signale. Le nom du dossier est l'identifiant du skin sur cette machine (1 à 64 caractères valides sous Windows, ni caché). Sans le skin choisi, le premier skin disponible est utilisé ; sans aucun skin, le moteur joue ses propres valeurs par défaut (rectangles blancs, aucune image) avec un seul avis visible « Aucun skin trouvé : placez un dossier de skin dans skins/ ». Un dossier sansskin.tsn'est pas un skin (signalé sur la page Skins).
Un paquet contient skin.ts (script unique, sans import, 1 Mio au plus),
des images .png et des polices .ttf/.otf. Les fichiers sont toujours
nommés par des chemins relatifs au dossier (notes/rouge.png) : pas de ..,
de chemin absolu, de nom réservé Windows ; un lien symbolique ou une jonction
qui sortirait du dossier est refusé.
Le script
export default defineSkin({
id: "neon", // règles d'un id de mod
name: "Neon",
version: "1.0.0",
apiVersion: 2, // version de l'API des mods du jeu
author: "Moi",
uses: { "pvng.judgement-display": "^1" },
setup(play: PlayContext) {
playfield.set({ laneWidth: 0.07, noteColor: "#ff4080", scroll: "up" });
lanes.set({ column: 0, lane: { note: "notes/gauche.png" } });
// HUD : hud.text(...), hud.group(...), voir docs/modding.md
},
});
defineSkin accepte aussi images (images chargées pour la partie, voir
plus bas) et permissions (["stage"] pour le dessin natif).
setup(play) reçoit le contexte du lancement (PlayContext ; ctx est
déjà le nom global de ctx.on(...)) : mode, layout (disposition du
catalogue des modes), layoutSkin (son apparence, par exemple 4k),
columns, screenWidth/screenHeight (pixels), song et judgements
(paliers de la partie). Le script est rechargé à neuf à chaque
lancement : rien ne s'accumule d'une partie à l'autre.
playfield.set(spec)fusionnespec(unPlayfieldSpec) dans la description du playfield : les champs donnés remplacent ceux des appels précédents, les autres restent.lanes.set({ column, lane })fusionne unLaneSpecdans la colonnecolumn(à partir de 0, 32 colonnes au plus).- Un champ jamais donné garde l'apparence par défaut du moteur de la disposition lancée :
un script sans
playfield.setdessine le playfield historique. - Le HUD n'est plus dessiné par le skin : chaque élément est fourni par un mod
(dossier de
mods/), et le skin le place et l'habille avecctx.element("<paquet>/<élément>").configure({ … }), après l'avoir déclaré dansuses(optionnel) et testé avecctx.has. Les valeurs qui changent à chaque jugement ou image (combo, précision, progression…) sont des liaisons (bind,fill,showWhen,animate,element) que les mods posent sur leurs nœuds et que la surcouche résout elle-même : voir modding. La section HUD deskins/default/skin.ts(et celle deskins/default-circle/skin.ts) sert d'exemple complet : elle ne crée aucun nœudhud.*. - Les éléments des mods du jeu :
pvng.accuracy/accuracy,pvng.combo/combo,pvng.counters/timer,…/remaining,…/hits,…/misses,pvng.progress-bar/progress,pvng.fps/fps,pvng.pause-status/status(options dans modding). Tous prennentx,y,anchor,sizeetvisible(sauf la barre :width,height,radius…), plus leurs couleurs. Le skin par défaut livrehitsetmissesavecvisible: false: la configuration du skin du joueur peut les afficher. Les positions et tailles qui dépendent de l'écran se calculent avecplay.screenWidthetplay.screenHeight, comme le fait le skin par défaut. - Le dernier jugement et les compteurs par palier viennent du mod
pvng.judgement-display, dans les couleurs des paliers de la partie. Un skin le déclare dansuses("^1", ou{ version: "^1", feature: "…" }pour nommer ce qui manque sans lui, comme le skin par défaut) puis le place et l'habille dans sonsetup:ctx.element("pvng.judgement-display/judgement").configure({ x, y, anchor, size, animation, colors })et…/counts(options dans modding). Sans le mod,ctx.hasvautfalse, un message l'indique sur la page Mods et le skin se passe de cet affichage (voir dépendances). - La hit bar est un autre mod,
pvng.hit-bar. Ajoutez"pvng.hit-bar": "^1"àuses, puis placez son élément sictx.has("pvng.hit-bar"):ctx.element("pvng.hit-bar/bar").configure({ x: 0.5, y: 0.427, anchor: "top", width: 0.42, height: 0.055, length: 50, visible: true }). Elle utilise les derniers offsets natifs non-Miss et des primitives HUD ordinaires. Sa présence ne dépend pas depvng.judgement-display. Les skins peuvent donc choisir séparément le dernier jugement, les compteurs et la visualisation des impacts.
Les types (PlayfieldSpec, LaneSpec, LaneUpdate, PlayfieldAnchor,
ScrollDirection, SpriteSize) sont définis en Rust dans crates/skin
(#[derive(TsSchema)]) et générés dans le SDK TypeScript des mods
(mods/sdk/modding.d.ts).
Référence du playfield
Positions en fractions de l'écran (x de sa largeur, y de sa hauteur,
depuis le coin haut-gauche) ; tailles en fractions de la hauteur de
l'écran : le playfield garde ses proportions à toute résolution.
| Champ | Bornes | Défaut | Rôle |
|---|---|---|---|
x, y |
0 à 1 | 0,5 ; 0,5 | position du point d'ancrage de la boîte des colonnes |
anchor |
topLeft … bottomRight |
center |
point de la boîte placé en x, y |
rotation |
−3600 à 3600 | 0 | rotation du playfield entier autour du centre de la boîte des colonnes, en degrés, sens horaire |
zoom |
0 à 4 | 1 | échelle du playfield entier autour du même centre |
laneWidth |
0,005 à 0,5 | 0,085 | largeur d'une colonne |
width |
0,005 à 2 | — | largeur totale ; fixe la largeur de colonne quel que soit leur nombre (prioritaire sur laneWidth) |
laneGap |
0 à 0,1 | 0,003 | espace entre deux colonnes : elles sont distantes de laneWidth + laneGap, l'écart ne rétrécit jamais une colonne |
laneHeight |
0 à 1 | 0,9 | hauteur de la boîte des colonnes |
receptorY |
0 à 1 | 0,85 (down), 0,15 (up) |
ligne des récepteurs, depuis le haut |
scroll |
down, up |
down |
les notes descendent vers les récepteurs, ou montent |
noteSize |
0 à 0,5 par côté | 0,0725 × 0,0725 | |
receptorSize |
0 à 0,5 | 0,0775 × 0,0775 | |
holdWidth |
0 à 0,5 | 0,02975 | largeur du corps des holds |
holdMatchNoteWidth |
booléen | false |
corps de hold de la largeur de la note de la colonne, embout de la largeur du corps (holdWidth et la largeur de holdEndSize ignorées) |
holdWidthScale |
0,25 à 3 | 1 | facteur de la largeur de note quand holdMatchNoteWidth est actif |
holdEndSize |
0 à 0,5 | 0,0725 × 0,015 | taille de la fin de hold (aussi par colonne) ; hauteur 0 : pas d'extrémité |
holdEndScale |
0,1 à 3 | 1 | multiplie la taille résolue de la fin de hold ; purement visuel |
holdEndFlip |
booléen | false |
retourne l'image de fin de hold par rapport à l'orientation automatique ; purement visuel |
holdEndOffset |
−0,5 à 0,5 | 0 | décale l'extrémité du hold le long du défilement, positif vers le récepteur ; purement visuel |
noteOffsetY |
−0,5 à 0,5 | 0 | décale notes et têtes de hold le long du défilement, positif dans le sens des notes ; purement visuel |
borderWidth |
0 à 0,1 | 0 | cadre autour de la boîte des colonnes |
borderColor |
couleur | blanc | couleur du cadre |
judgementLine |
booléen | false |
trace une ligne sur chaque colonne à la ligne de jugement (celle des récepteurs) |
judgementLineColor |
couleur | blanc | couleur de cette ligne |
judgementLineThickness |
0,0005 à 0,02 | 0,003 | épaisseur de cette ligne, en hauteurs d'écran |
judgementLineAt |
center, top, bottom |
center |
où la ligne se place sur les récepteurs : au milieu, sur leur bord haut ou leur bord bas (tels que l'écran les montre) |
laneCover |
booléen | false |
cache de colonnes (lane cover) : un bloc de couleur sur toute la largeur des colonnes, dessiné au-dessus des notes, côté apparition des notes (en haut quand elles tombent, en bas quand elles montent) ; il ne dépasse jamais le bord des récepteurs, qui restent visibles ; purement visuel (jugement, replays et timing des holds intacts) |
laneCoverColor |
couleur | #000000 |
couleur du cache |
laneCoverOpacity |
0 à 1 | 1 | multiplie l'alpha de la couleur du cache |
laneCoverSize |
0 à 1 | 0,3 | jusqu'où le cache descend depuis le bord de la boîte des colonnes où les notes apparaissent, en fraction de la hauteur de cette boîte |
laneCoverFeather |
0 à 0,2 | 0 | bord adouci : la fin du cache s'estompe sur cette longueur (fraction de la hauteur de la boîte, jamais plus que le cache lui-même) ; 0 : bord net |
measureLines |
booléen | false |
lignes de mesure : une ligne sur toute la largeur du playfield à chaque début de mesure de la map, qui défile comme les notes (même vitesse, même sens, zoom et rotation compris), dessinée sous les récepteurs et sous les notes, au-dessus du fond des colonnes ; purement visuel |
measureLineColor |
couleur | blanc | couleur des lignes de mesure |
measureLineOpacity |
0 à 1 | 0,35 | multiplie l'alpha de cette couleur |
measureLineThickness |
0,0005 à 0,02 | 0,003 | épaisseur des lignes de mesure, en hauteurs d'écran |
measureLineLength |
playfield, lanes |
playfield |
playfield : une ligne continue sur toute la largeur du playfield (les espaces entre colonnes remplis) ; lanes : un segment par colonne, de la largeur de la colonne, sur la ligne de réception de cette colonne (offsetY compris) |
measureLineOvershoot |
0 à 0,5 | 0 | dépassement de part et d'autre des colonnes extérieures, en hauteurs d'écran (playfield seulement) |
measureLineEvery |
1 à 16 (entier) | 1 | une mesure sur N est accentuée (deux fois plus épaisse, à l'alpha plein de la couleur), en comptant depuis la première mesure ; 1 : aucune accentuation |
beatLines |
booléen | false |
lignes de temps : une ligne plus discrète à chacun des autres temps de la mesure, sous les lignes de mesure |
beatLineColor |
couleur | blanc | couleur des lignes de temps |
beatLineOpacity |
0 à 1 | 0,12 | multiplie l'alpha de cette couleur |
beatLineThickness |
0,0005 à 0,02 | 0,002 | épaisseur des lignes de temps, en hauteurs d'écran |
backgroundColor |
couleur | transparent | fond de la boîte des colonnes, visible entre les colonnes ; teinte backgroundImage |
backgroundImage |
chemin .png |
aucune | image étirée sur la boîte des colonnes |
backgroundTint |
couleur | #818181 |
multiplie l'image de fond de la map (22 %, l'assombrissement historique) |
laneColor |
couleur | #454d61 |
fond de chaque colonne ; teinte laneImage |
noteColor, receptorColor, pressedColor, holdBodyColor, holdEndColor |
couleur | blanc | teintes multipliées avec l'image |
holdMissedStyle |
tint, hide |
tint |
une note longue dont la tête n'a jamais été touchée, ou relâchée trop tôt (état « Miss » du cœur de jeu, définitif pour la note), reste à l'écran assombrie jusqu'à sortir de l'écran (tint, y compris si le joueur rappuie), ou disparaît comme avant (hide) ; également par colonne |
holdMissedColor |
couleur | #808080 |
teinte multipliée avec la tête, le corps et l'extrémité d'une note longue ratée (les couleurs se multiplient en lumière linéaire : #808080 garde environ un cinquième de la luminosité) ; également par colonne |
holdMissedOpacity |
0 à 1 | 1 | multiplie l'alpha d'une note longue ratée ; également par colonne |
note, receptor, receptorPressed, holdBody, holdEnd |
chemin .png |
rectangle blanc, sans image | images de toutes les colonnes |
laneImage |
chemin .png |
aucune | image étirée sur chaque colonne |
keyLightOn |
booléen | true |
lumière de touche : le récepteur réagit à sa touche, même sur une colonne sans note ; false : il ne réagit pas (l'image receptor reste affichée) |
receptorPressed |
chemin .png |
receptor |
image de la lumière de touche (le récepteur pressé) |
pressedColor |
couleur | blanc | couleur de la lumière de touche |
keyLightOpacity |
0 à 1 | 1 | multiplie l'alpha de la couleur de la lumière de touche |
keyLightScale |
0,25 à 3 | 1 | échelle du récepteur pressé autour de son centre (1 : sa taille) |
keyLightFadeMs |
0 à 500 | 0 | durée (ms) de l'apparition et de la disparition de la lumière de touche ; 0 : instantané, comme avant |
stageLightOn |
booléen | true |
éclairage de colonne ; false n'en dessine aucun, même avec une image stageLight |
stageLight |
chemin .png |
aucune | image d'éclairage de chaque colonne tant que sa touche est tenue |
stageLightColor |
couleur | blanc | teinte de stageLight |
stageLightOpacity |
0 à 1 | 1 | multiplie l'alpha de l'éclairage de colonne |
stageLightSize |
0 à 0,5 par côté | 0 × 0 | taille de l'éclairage ; 0 : largeur de la colonne, hauteur selon l'image |
stageLightScale |
0,1 à 3 | 1 | facteur appliqué à la taille résolue ; le bord proche des récepteurs reste sur leur ligne |
stageLightOffsetY |
−0,5 à 0,5 | 0 | décale l'éclairage le long du défilement (positif : sens des notes) |
stageLightFadeMs |
0 à 500 | 100 | durée (ms) de l'apparition et de la disparition de l'éclairage de colonne ; 0 : instantané |
hitLight |
chemin .png avec {n} |
aucune | éclat de frappe : animation jouée une fois au niveau des récepteurs de la colonne, à chaque jugement de cette colonne qui n'est pas un Miss (note, tête de hold, relâchement de hold jugé), relancée par la frappe suivante ; {n} est remplacé par 0, 1, 2… |
hitLightOn |
all, off |
all |
hitLight joue à chaque frappe (all) ou jamais (off) ; le holdLight est un effet à part, il boucle tant qu'une note longue est tenue |
hitLightFollowJudgement |
booléen | false |
hitLight prend la couleur du palier touché (couleurs du jugement actif) au lieu de hitLightColor ; l'opacité reste appliquée |
hitLightFrames |
1 à 32 (entier) | 1 | nombre d'images de hitLight |
hitLightFps |
1 à 240 | 60 | images par seconde (temps réel) |
hitLightSize |
0 à 0,5 par côté | 0 × 0 | 0 : deux fois la largeur de la colonne, hauteur selon l'image |
hitLightOffsetY |
−0,5 à 0,5 | 0 | décale l'animation le long du défilement (positif : sens des notes) |
hitLightColor |
couleur | blanc | teinte de l'animation |
hitLightOpacity |
0 à 1 | 1 | multiplie l'alpha de l'animation |
hitLightScale |
0,1 à 3 | 1 | facteur appliqué à la taille résolue (autour du centre de l'image) |
holdLight, holdLightFrames, holdLightFps, holdLightSize, holdLightOffsetY, holdLightColor, holdLightOpacity, holdLightScale |
comme hitLight* |
aucune | lumière de hold : animation en boucle tant qu'un hold de la colonne est tenu (de sa tête jugée sans Miss à son relâchement ou sa fin) |
textureFilter |
linear, nearest |
linear |
filtrage des textures du playfield |
lanes |
32 au plus | — | valeurs par colonne (LaneSpec) |
LaneSpec a les treize champs d'image et de teinte des colonnes ci-dessus
(note, receptor, receptorPressed, holdBody, holdEnd, laneImage,
stageLight, laneColor, noteColor, receptorColor, pressedColor,
holdBodyColor, holdEndColor, stageLightColor) pour une seule colonne, les
valeurs noteOffsetY, holdEndOffset, holdEndSize, holdEndScale,
holdEndFlip, stageLightOffsetY, stageLightSize,
holdMatchNoteWidth et holdWidthScale de la colonne, les réglages de la
lumière de touche et de l'éclairage de colonne (keyLightOn, keyLightOpacity,
keyLightScale, keyLightFadeMs, stageLightOn, stageLightOpacity,
stageLightScale, stageLightFadeMs), l'aspect des notes longues ratées de la
colonne (holdMissedStyle, holdMissedColor, holdMissedOpacity), et offsetX (−2 à 2 hauteurs d'écran,
0 par défaut) qui décale toute la colonne vers la droite (fond, récepteur,
notes, holds). receptorPressed absent : l'image
receptor du même niveau sert aussi quand la touche est enfoncée. Les
couleurs s'écrivent #rgb, #rgba, #rrggbb ou #rrggbbaa ; une teinte
multiplie son image dans le shader. Une note parcourt 0,8 hauteur d'écran
pendant le temps de défilement réglé par le joueur.
La ligne de jugement (judgementLine) est un segment par colonne, de la
largeur exacte de la colonne, centré sur la ligne des récepteurs de cette
colonne (décalage offsetY compris) : il suit donc le zoom, la rotation et le
placement libre des colonnes, se dessine au-dessus du fond des colonnes et sous
les récepteurs, et ne change ni le jugement ni la position des hits. Comme les
autres valeurs, elle se règle par le script du skin, le script de la map, puis
par la configuration du joueur (éditeur de skin, groupe « Ligne de jugement »).
Le cache de colonnes (laneCover) est un seul bloc pour tout le playfield, sur la
largeur de la boîte des colonnes, qui suit le zoom et la rotation. Il est
dessiné après les notes, donc par-dessus elles, mais jamais sur la zone des
récepteurs : quelle que soit laneCoverSize, il s'arrête au bord des
récepteurs le plus proche de l'apparition des notes. L'adoucissement
(laneCoverFeather) est rendu par 24 bandes d'alpha décroissant dans le même
lot d'instances que le reste (pas de shader dédié). Le joueur le règle en direct
dans l'éditeur de skin, dans un onglet à part, « Cache des colonnes »
(groupe laneCover, à côté de « Ligne de jugement »), position
(laneCoverSize) et couleur comprises. Ce groupe fait partie du catalogue de
l'hôte (field_catalog), pas du skin : tous les skins l'ont, le skin par défaut,
Default Circle, ceux du joueur, y compris un skin minimal, désactivé par défaut.
Il se règle sur le playfield (pas sur une colonne : sélectionner une colonne
n'affiche que ses propres champs). Réglages → Vidéo → Playfield montre son état
pour le skin actif (désactivé, activé avec sa taille, ou « selon le skin » tant que
le joueur n'a rien choisi) et un bouton-icône « Ouvrir dans l'éditeur de skin » :
il envoie editLayout { focus: "laneCover" } et l'éditeur s'ouvre sur le
playfield, onglet « Cache des colonnes ». Les valeurs restent dans
skinConfig[skinId] : la page Réglages n'en garde aucune copie. Rien n'est jugé ni
enregistré différemment : seul le rendu change.
Avec l'option du joueur « Suivre les changements de vitesse (SV) » (docs/game.md, désactivée par défaut), les lignes de mesure et de temps suivent la même carte de défilement que les notes ; les valeurs du skin ne changent pas.
Les lignes de mesure (measureLines) et de temps (beatLines) sont natives : le
moteur les dessine, sans mod ni primitive de scène. Elles sont désactivées par
défaut, y compris dans le skin default (un skin les allume avec
measureLines: true dans son setupPlayfield, la map avec playfield.update, le
joueur dans l'éditeur). Le calendrier des lignes est calculé une fois au
lancement de la partie, sur le fil du moteur (pas celui du rendu), à partir des
points de timing de la chart déjà mise à la vitesse de jeu : un point BPM
commence une mesure, les temps suivent toutes les 60 / bpm secondes jusqu'au
point suivant, une mesure compte signature temps (1 à 64, sinon 4), et avant le
premier point son tempo se prolonge vers l'arrière jusqu'à 0. Les lignes sont donc
justes à n'importe quel rythme (rate) et suivent les changements de BPM. Même
sémantique que game.beat des mods. À chaque image, le rendu ne fait que deux
recherches dichotomiques dans ce calendrier (borné à 100 000 lignes) pour la
fenêtre visible, sans allocation, et dessine au plus 256 lignes de mesure et 256
lignes de temps : la position d'une ligne est celle d'une note du même instant
(temps de défilement, sens, décalage visuel, zoom, rotation, receptorY), dans le
même lot d'instances que le reste. Elles apparaissent dans le jeu, le replay, le
visualiseur, l'aperçu et la session d'édition du skin (la démo a un BPM : 150), pas
dans l'éditeur de map, qui a sa propre grille. Ce qui est supporté, selon la source :
- osu! : chaque point de timing non hérité donne son BPM et sa signature
(
meter) ; une signature absurde vaut 4. - StepMania / Etterna (.sm, .ssc) : les changements de BPM comptent, la mesure a
toujours 4 temps (
#TIMESIGNATURESn'est pas lu). Les arrêts (#STOPS), délais et warps sont déjà intégrés aux temps des notes par l'import, sans point de timing : les lignes ne les montrent pas (elles suivent le tempo seul). - Une chart sans point BPM n'a pas de lignes ; deux points au même instant : le dernier gagne ; un BPM au-delà de 60 000 est ignoré.
Les valeurs restent dans skinConfig[skinId] (précédence : script du skin, script
de la map, configuration du joueur, en direct). Réglage dans l'éditeur de skin,
onglet « Lignes de mesure » (groupe measure, deux sections : « Lignes de
mesure » et « Lignes de temps »), sur le playfield uniquement : aucune de ces
valeurs n'existe par colonne. Rien n'est jugé ni enregistré différemment.
Les notes longues ratées (holdMissedStyle) lisent l'état déjà connu du cœur de
jeu (tête jamais touchée, ou relâchement trop tôt) sans le modifier ; ni le
jugement ni le format des replays ne changent, et le replay montre donc la même
chose que la partie. Dans l'éditeur de skin (groupe « Couleurs », section
« Notes longues ratées »), la démo d'édition affiche toujours un hold comme s'il
était raté (le premier dont la tête est encore devant les récepteurs, sans rien
juger) pour voir l'effet en direct.
Largeur des holds suivant les notes. Avec holdMatchNoteWidth (booléen,
false par défaut ; les skins default et default-circle le mettent à
true), le corps d'un hold fait la largeur de la note de sa colonne (sa taille
résolue : noteSize du skin, de la map, du joueur ou de la colonne) multipliée
par holdWidthScale (0,25 à 3, 1 par défaut) et l'embout fait la largeur du
corps ; holdWidth et la largeur de holdEndSize sont alors ignorées. La hauteur
de l'embout suit les proportions de son image, sauf si un skin ou le joueur
donne holdEndSize (sa hauteur s'applique alors). Agrandir les notes dans
l'éditeur agrandit donc corps et embouts ensemble, en direct. Les deux valeurs
existent aussi par colonne (LaneSpec).
Décalages visuels. noteOffsetY et holdEndOffset (−0,5 à 0,5 hauteur
d'écran, 0 par défaut) déplacent des images le long de l'axe de défilement, sans
jamais toucher au jugement, au timing des holds, au score ni aux replays (comme
le décalage visuel du joueur). noteOffsetY déplace les notes et les têtes de
hold : positif, dans le sens où les notes avancent (vers les récepteurs et
au-delà) ; négatif, vers l'arrière. 0 centre la note sur la ligne des
récepteurs, comme dans Default Circle où la ligne de jugement est le milieu de
la note. Le corps et
l'embout du hold ne suivent pas : ils restent sur le temps de la fin.
holdEndOffset déplace l'image d'extrémité : positif, vers le récepteur
(« 30 px avant la fin » est une valeur positive), négatif, à l'opposé. Une
extrémité dont la fin est hors de la zone dessinée n'est pas dessinée (elle
n'est plus collée au bord de l'écran). L'image d'extrémité s'écrit comme une
note : sa pointe (le bas de l'image) vise les récepteurs quand les notes
descendent, comme dans les skins osu! ; le jeu la retourne pour qu'elle pointe à
l'opposé des récepteurs dans le repère du playfield : retournée quand les
notes descendent, telle quelle quand elles montent, pour toute rotation et tout
zoom.
Fin de hold (onglet de l'éditeur). L'image de fin d'un hold a son propre
groupe dans le catalogue de l'éditeur (holdEnd, une seule section holdEnd,
onglet « Fin de hold ») : holdEnd (image), holdEndColor (teinte),
holdEndSize (taille, par colonne aussi), holdEndScale (0,1 à 3, 1 par défaut :
multiplie la taille résolue, la position de la fin ne bouge pas), holdEndOffset
(décalage le long du défilement) et holdEndFlip (booléen, false : retourne
l'image par rapport à l'orientation automatique ci-dessus). Chaque champ existe
pour tout le playfield et par colonne (LaneSpec), avec la précédence habituelle
(skin < script de la map < joueur ; la valeur propre d'une colonne l'emporte sur celle
du playfield de la même couche), en direct dans l'éditeur, et reste purement
visuelle : jamais de jugement, de timing de hold, de score ni de replay. La
largeur suit le corps quand holdMatchNoteWidth est actif ; sa hauteur vient de
l'image sauf si holdEndSize la donne. Dans la démo d'édition, chaque fin de hold
à l'écran est un cadre qu'on clique pour la sélectionner (surlignage aux couleurs
du thème, onglet « Fin de hold » seul, valeurs de cette colonne) ; on la fait
glisser le long de la colonne (ou avec les flèches haut/bas) pour régler
holdEndOffset, Suppr remet les valeurs de fin de hold de la colonne à celles du
skin. La ligne « Fin de hold » de la liste des calques sélectionne les fins de toutes
les colonnes (valeurs du playfield) ; l'œil masque les cadres. Les cadres viennent
de l'hôte (editLayout.holdEnds, des HoldEndBox normalisées comme laneBoxes),
calculés par le même code que le dessin (Layout::hold_end_rect), donc le cadre
est exactement l'image dessinée (zoom compris).
Éclairage de colonne (stage light). Avec une image stageLight, chaque
colonne s'éclaire tant que sa touche est tenue : un quad de la largeur de la
colonne (stageLightSize.width à 0) et de la hauteur de
stageLightSize.height, ou du rapport de l'image si elle vaut 0, multipliés par
stageLightScale, teinté par stageLightColor et d'alpha multiplié par
stageLightOpacity, dessiné au-dessus du récepteur et sous les notes, son bord le
plus proche des récepteurs sur la ligne des récepteurs de la colonne (décalage
stageLightOffsetY compris, positif dans le sens de défilement des notes) et
s'étalant du côté d'où viennent les notes. Son alpha monte de 0 à 1 touche tenue
et retombe linéairement après le relâchement en stageLightFadeMs (100 ms par
défaut ; 0 : instantané), en temps réel de l'image, calculé par le moteur de
rendu sans allocation ni script par frame. stageLightOn (booléen, true par
défaut, couches skin < map < joueur, en direct, par colonne aussi) le coupe sans
toucher à l'image, à la couleur ni aux animations. Sans image stageLight
(défaut) rien n'est dessiné. Le skin Default Circle n'en a pas : ses images
d'éclairage (lightingN, lightingL, lighting, mania-stage-light) sont des
pixels transparents de 1×1 dans le .osk d'origine.
Lumière de touche (key light). C'est la réaction du récepteur à sa touche,
même sur une colonne sans note : le mécanisme du récepteur pressé, dont les noms
historiques sont conservés. Correspondance : image de la lumière de touche =
receptorPressed, couleur = pressedColor, auxquels s'ajoutent keyLightOn
(false : le récepteur ne réagit pas à la touche), keyLightOpacity (alpha de
la couleur), keyLightScale (échelle du récepteur pressé autour de son centre)
et keyLightFadeMs (fondu, 0 par défaut : le changement instantané d'avant).
Pendant un fondu, le récepteur normal reste dessiné sous l'image pressée, dont
l'alpha suit l'enfoncement ; le fondu part du temps réel de l'image, comme celui
de l'éclairage de colonne. Tout cela existe aussi par colonne. L'éditeur les
regroupe dans la section « Éclairage de touche » du groupe « Éclairage ».
Renommage et migration des valeurs enregistrées. L'éclairage de colonne
s'appelait light, lightColor, lightSize et lightOffsetY ; keyLightOn a
brièvement désigné son interrupteur sans jamais être livré. Les configurations
du joueur déjà enregistrées avec les anciens noms light* (playfield, colonnes
et modifications) sont lues une seule fois sous les nouveaux noms stageLight*
(alias de désérialisation, migration ponctuelle de données enregistrées) et
réécrites sous ces nouveaux noms à la prochaine sauvegarde. Il n'y a aucun autre
alias : les scripts de skin et de map doivent utiliser stageLight*, et l'ancien
keyLightOn n'est pas migré (il désigne maintenant la lumière de touche).
hitLightOn. all (défaut, comportement des autres skins) joue l'éclat à
chaque jugement non Miss de la colonne. holds ne le joue que pour la tête
d'un hold touchée (non Miss) : jamais pour une note simple, jamais à un Miss, et
pas non plus au relâchement, même réussi (tête seule). Le moteur de rendu le
déclenche quand un hold de la colonne commence à être tenu, l'état qui pilote
déjà la lumière de hold, donc quel que soit le modèle de holds du jugement (avec
les modèles à jugement unique, la tête n'émet aucun jugement). Limite : un hold
commencé et relâché entre deux images n'affiche pas l'éclat. Se règle comme les
autres valeurs (skin < map < joueur, en direct, groupe « Éclairage » de l'éditeur).
off ne dessine aucun éclat de frappe, quel que soit le skin : le joueur peut ainsi
désactiver l'éclat bleu sans toucher à la lumière de hold (boucle, réglée par
holdLight) ni à l'éclairage de colonne (stageLight) ni à la lumière de touche, qui gardent leurs propres champs.
Éclat de frappe et lumière de hold. hitLight et holdLight sont des
animations par image, sur toutes les colonnes (elles n'existent pas par
colonne, contrairement à stageLight). Un motif de chemin porte le jeton {n},
remplacé par 0, 1, 2… pour chaque image (fx/spark-{n}.png) ; …Frames en
donne le nombre (1 par défaut, 32 au plus) : une liste d'images dans l'éditeur
demanderait un nouveau type de champ, alors que le motif tient dans un champ
texte et un nombre, et se règle aussi pour un skin du joueur
(player:boom-{n}.png). Un seul fichier suffit pour une animation d'une
image (…Frames à 1, sans jeton). Chaque image est un PNG du dossier du skin ;
le motif est vérifié en Rust (chemin sûr, .png, {n} obligatoire au-delà
d'une image), au plus 256 images distinctes par couche. L'éclat se joue une fois
depuis son image 0 à …Fps images par seconde, redémarre à chaque nouveau
jugement non Miss de la colonne, puis disparaît ; la lumière de hold boucle
tant qu'un hold de la colonne est tenu et s'arrête au relâchement ou à la
perte du hold. Le temps est le temps réel de l'image (comme le fondu de stageLightFadeMs :
la cadence ne dépend ni du rate ni de l'horloge, figée dans l'éditeur), sans
allocation ni script par frame. L'éclat part des jugements que le moteur de
rendu reçoit déjà pour les stage.* (colonne, Miss ou non) et la lumière de hold
de l'état des holds de la partie : rien de tout cela ne touche au jugement, au
score ni aux replays. Les images sont centrées sur la ligne des récepteurs de la colonne
(décalage …OffsetY compris, positif dans le sens des notes), au-dessus des
récepteurs et de l'éclairage de colonne, sous les notes, retournées comme stageLight quand les
notes montent (écrites pour des notes qui descendent). Largeur : 2 colonnes
si …Size.width vaut 0, hauteur selon les proportions de l'image courante.
Les couches se résolvent par valeur (script du skin < script de la map <
configuration du joueur, en direct dans l'éditeur, groupe « Éclairage »). Le
motif, retenu au niveau le plus haut dont la première image existe, prend autant
d'images consécutives qu'il en trouve jusqu'au nombre demandé (le plus haut
niveau qui en donne un, sinon 1) : une image manquante est signalée et
raccourcit l'animation. Le mélange est le mélange alpha normal du moteur (un
seul lot d'instances, pas de shader propre) : un mélange additif n'y entre pas,
et les images d'éclat de default (cyan clair, alpha jusqu'à 254) s'y
affichent correctement sur le fond sombre.
Le filtrage des textures est un réglage du skin, textureFilter : linear
(par défaut) échantillonne en bilinéaire, les sprites agrandis ou réduits
restent lisses ; nearest garde des pixels nets (skins en pixel art). L'atlas
réplique le bord de chaque image dans une gouttière de 2 px qui lui est propre
(les pixels totalement transparents prennent la couleur de leurs voisins
opaques) : aucun voisin ne déborde et aucune frange sombre n'apparaît au
bord. Pas de mipmaps : le bilinéaire convient jusqu'à une réduction d'environ
2× (les images sont ramenées à 640 px au plus). Le joueur le change dans
l'éditeur de skin (groupe « Rendu »), en direct ; le rendu natif des polices
reste distinct.
Apparence par défaut du moteur (valeurs absentes ; le skin default définit toutes les siennes) :
les colonnes prennent tour à tour les flèches left, down, up, right du
skin default (une chacune en 4K, répétées au-delà), chacune avec la touche
de sa flèche (keys/<flèche>.png et keys/<flèche>-d.png), le corps
notes/body.png et l'embout notes/tail-cap.png. Ces touches sont hautes
(198 × 566 px, flèche au centre) : sans receptorSize du skin elles sont
étirées dans le carré de 0,0775 de l'apparence par défaut du moteur, il faut donc donner
une hauteur de 2,86 fois la largeur comme le fait skins/default/skin.ts. La
couleur des colonnes, historiquement une valeur linéaire, est l'arrondi 8 bits
#454d61 (écart
< 0,002).
Résolution et erreurs
Chaque valeur d'une colonne se résout dans l'ordre : lanes[colonne], le champ
de playfield.set, puis l'apparence par défaut du moteur. Le script de la
map, s'il y en a un, dessine une couche au-dessus : chaque
valeur qu'il donne l'emporte sur celle du skin. Rien d'invalide n'empêche la
partie :
- nombre hors bornes : ramené dans les bornes ; non fini : valeur par défaut ;
- couleur ou chemin invalide : valeur suivante de la chaîne ;
- image absente, illisible, non PNG, de plus de 4 Mio ou de plus de 2048 px de côté : pas d'image (rectangle blanc) ;
- colonnes en trop dans
lanes: ignorées ; - script en erreur,
defineSkininvalide ousetupau-delà du budget : le premier skin disponible est joué à sa place (sans aucun skin, les valeurs par défaut du moteur) ; sans réponse du thread des mods en 1 s, la partie démarre avec l'apparence par défaut du moteur.
Chaque problème est signalé : message dans le journal des mods pendant
setup, bandeau d'erreur au lancement et liste sur la page Skins (le skin
utilisé affiche les problèmes de son dernier lancement).
Couche du joueur
Au-dessus du skin et du script de la map, le joueur personnalise chaque skin
séparément (éditeur de skin, réglage skinConfig[skinId] des paramètres, voir
Configuration du joueur par skin) : tout le
playfield, colonne par colonne, et les widgets du HUD. Chaque valeur présente
l'emporte sur celle du skin et de la map, y compris les playfield.update en
cours ; les autres suivent le skin. Les valeurs hors bornes sont ramenées dans
les bornes, les non finies retirées. La couche s'applique à chaque lancement d'une
partie et en direct pendant l'édition : la séance d'édition (Command::EditLayout)
joue la démo d'édition figée à 2 s (Song::edit_demo, réservée aux séances
d'édition : des notes simples et des holds de longueurs différentes, dont un dont
la tête est sur la ligne des récepteurs, un dont le corps la traverse et un dont la
fin tombe exactement dessus, pour voir et régler corps, tête, fin de hold (au moins
trois colonnes en ont une à l'écran), largeur des holds, noteOffsetY),
sans jugement, sans score ni replay, jusqu'à Échap. Un aperçu des lumières, activé
au début de la séance (overlayEditPreview { lights }, état renvoyé dans
editLayout.previewLights), fait dessiner par le moteur de rendu, sans rien
juger, la lumière de touche (récepteurs pressés) et l'éclairage de colonne sur toutes les colonnes, la lumière de hold sur les
colonnes dont un hold traverse la ligne des récepteurs et l'éclat de frappe en
boucle (relancé toutes les 1,2 s, selon hitLightOn : all toutes les colonnes,
holds celles qui ont un hold, off aucune ; keyLightOn et stageLightOn s'appliquent) ; désactivé, le playfield reste net.
Changer le playfield pendant la partie
Depuis ses gestionnaires d'événements (ctx.on("game.judgement", …),
game.tick, game.pause…, enregistrés dans setup), un skin change
n'importe quel champ du playfield, toutes les couleurs et toutes les images
comprises :
setup(play: PlayContext) {
ctx.on("game.judgement", (judgement) => {
lanes.set({ column: judgement.column, lane: { receptorColor: judgement.tier.color },
transitionMs: 0 });
if (judgement.combo % 100 === 0) {
playfield.update({ patch: { note: "notes/gold.png", laneWidth: 0.09 },
transitionMs: 300, easing: "easeOut" });
}
});
}
playfield.update({ patch, transitionMs?, easing? }):patchest unPlayfieldSpecpartiel (les champs donnés changent,lanescompris).lanes.set({ column, lane, transitionMs?, easing? })fait de même pour une colonne.transitionMsva de 0 à 10 000 (absent ou 0 : immédiat) ;easingvautlinear(défaut),easeIn,easeOutoueaseInOut.- Nombres et couleurs passent de la valeur affichée à la nouvelle pendant la transition ; les images, l'ancrage et le sens de défilement changent tout de suite. Chaque valeur a sa propre transition : un changement n'interrompt que celles des valeurs qu'il modifie. La transition suit l'horloge de la chanson : elle s'arrête pendant une pause.
- Images préchargées : seules les images chargées au lancement peuvent
être affichées : celles que le
playfield.setdesetupnomme et celles quedefineSkin({ images: [...] })déclare (64 au plus). Une image non chargée lève une erreur dans le script ; le rendu ne décode jamais rien. - Chaque appel est validé (bornes, couleurs, chemins, colonne < 32) ; une erreur est levée dans le script. Le nombre de colonnes ne change pas et le jugement n'est pas affecté : ces changements sont visuels.
- Les changements d'un même pas du thread des mods (un lot d'événements) qui
ont la même transition sont fusionnés en un
PlayfieldPatch(les derniers champs gagnent) ; ceux de transitions différentes restent séparés, dans l'ordre (8 au plus par pas, au-delà ils rejoignent le dernier). Ils partent par une file bornée de 64 que le thread des mods remplit sans jamais attendre ; si elle est pleine, les changements sont gardés pour le pas suivant. - Le thread de rendu vide la file au début de chaque image, résout le nouveau playfield avec les images déjà dans l'atlas (aucun envoi de texture : les teintes sont des multiplications du shader, la teinte de fond est un uniforme de 16 octets) et anime la transition image par image. Sans changement ni transition, une image lit une case de la file et n'alloue rien.
- L'entité
game.playfield()des mods reste la disposition du lancement.
Dessin natif : stage.*
Un skin qui déclare permissions: ["stage"] dans defineSkin peut aussi
dessiner dans le rendu natif avec stage.*, à l'image près avec les notes :
sprites, rectangles, textes, émetteurs de particules et déclencheurs évalués
par le thread de rendu (référence : modding).
Sans la permission, chaque appel stage.* lève une erreur. Un sprite ou un
émetteur ne peut nommer qu'une image déclarée dans defineSkin({ images }) ;
ces images sont décodées avec celles du playfield, dans le même atlas. Comme
le reste du script, les éléments du skin repartent de zéro à chaque lancement
de map.
Chargement sans ralentir le jeu
lancement de map (thread principal)
└─ thread d'aide :
files des changements de la partie : skin::patch_channel(), une pour le
skin, une pour le script de la map
prepare_play(PlayContext, émetteurs) ──> thread mods : skin.ts rechargé,
setup(play), puis le script de la map (voir map-scripts.md)
attend les PlayfieldSpec et la liste des images (1 s au plus)
skin::prepare : validation, décodage PNG (skin, script de la map,
images stage des mods), un seul atlas
└─ thread principal : RenderCommand::Skin { prepared, patches,
chart_patches } puis Start
thread de rendu : un upload de texture avant la première image ;
à chaque image : changements reçus, transitions, dessin
Les images (celles du playfield.set et les images déclarées) sont décodées puis réduites (en
gardant leurs proportions) à 640 px de côté au plus et rangées dans un atlas
de 2048 px de large et de 4096 px de haut au plus ; 256 images au plus par couche (skin, script de la map), une image
qui n'y tient pas est ignorée (rectangle blanc). Le thread de rendu ne reçoit que l'atlas prêt et la
disposition résolue : il ne lit ni ne décode rien, et une image sans
changement n'alloue rien.
Convertir un skin osu!
Le mod mods/skin-converter ajoute à la page Skins une carte « Convertir un skin osu! » : le joueur
choisit un dossier de skin osu!mania ou un .osk, la carte montre les dispositions 4K à 7K qui seront
créées, ce que Prism ne sait pas faire (tête de hold distincte, NoteBodyStyle ≠ 0, lignes de colonne,
images de jugement, de combo et de score, sons…) et le poids du skin ; « Créer le skin » écrit un dossier
ordinaire dans <données>/skins/ (jamais en remplaçant un skin existant), puis « Ouvrir dans l'éditeur de
skin ». Les valeurs suivent celles du skin default converti à la main (ColumnWidth → laneWidth,
ColumnSpacing → laneGap, HitPosition → receptorY au milieu des notes, KeyImage calées sur la place
qu'osu! leur donne, StageLight/LightingN/LightingL → éclairages, Colour{N} → laneColor, ScorePosition
et ComboPosition → HUD) ; les écarts connus sont listés dans le test d'or
crates/modding/tests/skin_converter.rs. Un nombre de touches sans section convertie joue avec la
disposition convertie la plus proche. Conception : convertisseur-skin-osu.md.
Gérer les skins
Page Skins (menu Échap) : liste avec aperçu (preview.png du skin, sinon
son left.png), nom, version et auteur déclarés par defineSkin, skin
utilisé, problèmes ; boutons Utiliser, Exporter, Désinstaller, Importer…, Ouvrir le dossier, Recharger. Changer de skin redémarre l'hôte des
mods (hors partie) ; il sert à partir du lancement suivant.
- Réglage
skin(nom de dossier,defaultpar défaut), enregistré avec les autres réglages et validé par Rust : un nom invalide devientdefault; un dossier absent est signalé et le premier skin disponible est utilisé. Chaque skin a sa propre configuration du joueur (skinConfig[<id>]).prism.exe --skin <id>(benchmark, smoke test) choisit n'importe lequel. - Désinstaller supprime le dossier du skin, y compris un skin du jeu de
skins\à côté deprism.exes'il est accessible en écriture (sinon une erreur nomme le chemin). - Exporter écrit
<id>.zipdans un dossier choisi (fichiers cachés exclus ; tout autre fichier doit être.ts,.png,.ttfou.otf). - Importer installe un
.zipchoisi comme nouveau dossier, sans jamais remplacer un skin existant : nom du dossier racine de l'archive (ou du fichier), suffixé-2,-3… si besoin. Toute l'archive est vérifiée avant d'écrire : chemins relatifs sûrs (pas de zip-slip, de chemin absolu, de\, de nom réservé), pas de lien symbolique ni de chiffrement, extensions autorisées, doublons de casse refusés, 64 Mio par archive, 512 fichiers, 32 Mio par fichier et 64 Mio au total, taille réelle de chaque fichier contrôlée pendant l'extraction ;skin.tsrequis à la racine (ou dans l'unique dossier racine). L'extraction se fait dans un dossier caché puis un renommage. - Recharger relit la liste ; les fichiers d'un skin sont relus à chaque lancement de map.
prism.exe --bench-gameplay 10 --skin <id> et --smoke-test --skin <id>
jouent avec le skin <id> (les réglages de l'interface arrivent après le
démarrage du benchmark).
Contrat avec l'interface
- Événement
{ type: "skins", dir, skins: [{ id, name, author, version, preview, errors }], selected, errors }:selectedest le skin du prochain lancement,errorsles entrées du dossier qui ne sont pas des skins,previewun chemin/skins/<id>/<fichier>servi par le protocoleprism. { type: "skinOperation", action: "import" | "export", id, path }après une opération réussie ; les échecs arrivent enerror.- Commandes
selectSkin { id },reloadSkins,openSkinsFolder,importSkin,exportSkin { id }.
Exemple : notes roses
- Copier le dossier
skins\defaultsous un autre nom (skins\rose) dans le dossier des skins du joueur, changeridetnamedans sonskin.ts, puis Recharger sur la page Skins. - Dans
rose\skin.ts, changernoteColor: "#ffffff"ennoteColor: "#ff4fa3"(ou remplacernotes/left.pngpar une autre image). - Utiliser ce skin et lancer une map.
Configuration du joueur par skin
Les réglages du joueur sont rangés par skin (Settings.skinConfig[skinId], 64 skins au plus) : playfield (SkinPlayfieldConfig) et elements (options des éléments HUD que le skin place). Le remplacement de l'ancien réglage global playfieldLayout est lu une seule fois : il est déplacé dans la configuration du skin sélectionné si celle-ci n'a pas de playfield, sinon abandonné.
- Couches du playfield, la plus haute gagne valeur par valeur, en direct et au-dessus des transitions en cours : skin < script de map < configuration du joueur. Elle couvre x, y, zoom, rotation, largeur/écart/hauteur des lanes, ligne des récepteurs, sens de défilement, tailles (notes, récepteurs, fins de hold), bordure, couleurs, teinte du fond et, lane par lane (
lanes, colonnes 0..31),offsetX,offsetY,width, tailles, couleurs et images. Les décalages d'une lane déplacent son fond, son récepteur, ses notes et ses holds, jamais le jugement. - Options d'un élément, un seul ordre : défauts déclarés <
configure()du skin <skinConfig[skin].elements(éditeur) ; une valeur globale ne l'emporte jamais sur l'éditeur. Les réglages propres des mods (elementOptions, optionsplayer: true) ne s'appliquent qu'à un élément que l'éditeur ne peut pas placer, et restent sousskinConfig. - Images : une référence est un PNG du dossier du skin ou
player:<fichier>(images importées par le joueur dans<données>/skins-config/<skin>/images/, PNG/JPEG/WebP, 4 Mo et 2048 px au plus, 64 par skin ; l'atlas les réduit à 640 px). La configuration du joueur est une couche séparée : elle ne modifie pas les fichiers du skin. - L'éditeur en jeu (
overlayEditSkin,overlayEditElement,overlayImportSkinImage) travaille sur une copie de la configuration du skin actif ; chaque geste validé (commit) l'enregistre et émetskinConfigChanged.
Personnaliser un skin en jeu
Page Skins → Personnaliser le skin (carte du skin en cours) ouvre l'éditeur de
skin : la démo figée tourne sous la surcouche, un clic sur le playfield, sur une
colonne ou sur un widget du HUD le sélectionne et l'inspecteur propose ce qui se
règle à cet endroit. Le catalogue des champs vient de l'hôte (fields, groupes
placement / taille / couleur / image / éclairage / ligne de jugement / rendu,
bornes et pas) : aucune borne n'est écrite deux fois dans l'interface. Chaque champ
porte une section (identifiant stable en camelCase : position, lanes, scroll,
notes, receptors, holds, background, keyLight, stageLight, hitLight, holdLight,
line, texture) : l'interface affiche un petit titre par section dans un onglet
(skin_editor.section_<id>), les champs d'une section étant listés ensemble dans
l'ordre du catalogue.
- Playfield : position (glisser), zoom, rotation, largeur, écart et hauteur des colonnes, ligne des récepteurs, sens de défilement, tailles des notes, des récepteurs et des holds, bordure, couleurs et teinte du fond.
- Colonne par colonne : chaque colonne se place librement (
offsetX,offsetYen hauteurs d'écran ; l'inspecteur les saisit en pixels de la fenêtre, « Centre X / Y » : l'une à 0 px, une autre à 100 px, une troisième à 1250 px), se redimensionne et a ses propres couleurs et images. - Images : note, récepteur, récepteur appuyé, corps et fin de hold, fond de
colonne et fond du playfield se choisissent parmi les images du skin ou
importées (
player:<fichier>), ou s'importent depuis le disque. - Widgets du HUD : ce sont des mods ; chacun se sélectionne, se
déplace, se redimensionne et se masque, par skin
(
skinConfig[skin].elements).
Un skin personnalisé se reconnaît sur la page Skins (badge, nombre de valeurs,
colonnes, widgets et images importées) ; Réinitialiser la personnalisation
retire sa configuration. Toutes les options d'un widget du HUD (décimales de la
précision, visibilité, polices, couleurs, animations…) se règlent ici, skin par skin :
l'éditeur est l'unique endroit. Réglages → Mods ne montre plus que les options
player: true d'un mod dont l'élément n'a pas de place (ni x ni y numériques :
l'éditeur ne peut pas le placer) ; la catégorie disparaît quand il n'y en a aucune.
Les anciennes valeurs globales d'un élément que l'éditeur place sont reprises une fois
dans la configuration du skin actif et des skins déjà configurés, sans jamais écraser
une valeur du skin.
Fonctions de skin manquantes
Proposition, rien de ceci n'est implémenté. Comparaison avec les réglages
skin.ini d'osu!mania (clés vérifiées sur le wiki d'osu!), les noteskins de
StepMania/Etterna et le skin.ini de Quaver (ces deux dernières sources de
mémoire, à confirmer avant de coder). Classées par intérêt décroissant ; effort :
S (quelques heures), M (une journée), L (plusieurs jours) ; risque : régression de
perf ou de format.
| # | Fonction | Qui l'a | Valeur | Effort | Risque |
|---|---|---|---|---|---|
| 1 | Notes colorées selon la subdivision (1/4 rouge, 1/8 bleu, 1/12 violet, 1/16 jaune…) : teinte ou image par subdivision, calculée à partir du calendrier de game-core::barlines |
StepMania/Etterna (noteskins par quantification), Quaver (ColorObjectsBySnapDistance) |
très forte : lecture de la rythmique, demandée par tous les joueurs de StepMania | M : une subdivision par note calculée au lancement (hors rendu), une teinte par subdivision dans la configuration ; images par subdivision en L | moyen : exactitude sur les changements de BPM, arrêts non pris en compte, une table de plus par partie |
| 2 | Séparateurs de colonnes, bordures et remplissage de scène (largeur et couleur individuelles) | osu! (ColumnLineWidth, ColourColumnLine, StageLeft/StageRight/StageBottom) |
forte : un des éléments les plus visibles d'un skin osu! | S : des quads fins entre les colonnes, sous les notes | faible |
| 3 | Position et taille du combo et du jugement | osu! (ComboPosition, ScorePosition), Quaver (ComboPosY, JudgementBurstPosY) |
moyenne | S : ce sont des widgets de mods ; en faire des options de mod, pas des champs du skin | faible |
| 4 | Variantes du hit light : image par tier de jugement, par colonne, hauteur propre (LightPosition) |
osu! (LightPosition, LightingN/LightingL), Quaver (HitLightingY, largeur/hauteur) |
moyenne : on a déjà hitLight/holdLight, la couleur du tier et le décalage |
S | faible |
| 5 | Images selon le type de note : accord (plusieurs notes au même instant), colonne spéciale | osu! (SpecialStyle, images NoteImage#), StepMania (jump) |
moyenne | M : regrouper les notes simultanées au lancement | moyen |
| 6 | Styles du corps des notes longues : étiré (actuel), répété depuis le haut ou le bas | osu! (NoteBodyStyle), StepMania (corps étiré ou répété, capuchons) |
moyenne | M : le shader n'a pas de répétition d'atlas, il faut des quads par tuile (bornés) | moyen : nombre de quads |
| 7 | Notes et récepteurs animés (suite d'images) | osu! (NoteImage#@N), StepMania, Quaver (AnimationFrameRate) |
moyenne : les lights le sont déjà, pas les notes | M : un index d'image par note à chaque image, dans le chemin chaud | moyen : perf, à mesurer avec --bench-gameplay |
| 8 | Image de touche par colonne sous ou sur les notes | osu! (KeyImage#, KeysUnderNotes), Quaver (ReceptorsOverHitObjects) |
moyenne | S à M : une couche par colonne et un ordre de dessin | faible |
| 9 | Cache des colonnes côté récepteurs (lift / « hidden ») et réglage en jeu | lecteurs de StepMania/Etterna (appearance), osu! (lazer : cover) | moyenne : le laneCover actuel ne couvre que côté apparition |
S | faible |
| 10 | Retourner les images des notes en défilement vers le haut (par type : tête, corps, queue) | osu! (NoteFlipWhenUpsideDown#H/L/T), Quaver (FlipNoteImagesOnUpscroll) |
faible à moyenne : on a déjà holdEndFlip |
S | faible |
| 11 | Mesures exactes pour StepMania : #TIMESIGNATURES, arrêts et warps comme points de timing dans l'import |
— (donnée, pas skin) | moyenne pour les lignes de mesure des fichiers .sm | M : étendre le format ROX (vendor/Rhythm-Open-Exchange) |
moyen : touche l'import et les replays |
osu! a aussi ColourBarline/BarlineHeight (lignes de mesure, sans image), ColourJudgementLine et
JudgementLine : déjà couverts par measureLines* et judgementLine*.
Ce qui devrait être un mod, ce qui reste moteur
Règle : moteur (natif) par défaut ; un mod seulement quand c'est une vraie
surcouche optionnelle, composée de primitives existantes, et qui vaut la peine
d'être amovible. Les lignes de mesure ont d'abord été envisagées comme un mod
(primitive de scène + requête sur la chart) : rejeté, elles s'appuient sur le
temps de défilement exact du rendu et un calendrier calculé au lancement, ce qui
est du moteur. Restent dans le moteur, sans changement : cache des colonnes
(laneCover), holdMissedStyle, ligne de jugement et son ancrage, lumières
(scène, touche, hit, hold), image de fin de hold, couleurs par colonne, lignes de
mesure et de temps. Sont déjà des mods : les widgets du HUD (jugement, précision,
combo, compteurs, barre de progression, FPS, ghost…). Le sélecteur de note
(RatingPicker) est de l'interface, pas du skin. Rien n'est à migrer.