# Prism (PVNG) : contexte pour écrire un mod TypeScript # Fichier unique à coller dans un assistant IA. API version 2. Source : docs/modding/*.md et mods/sdk/modding.d.ts. ## RÈGLE ABSOLUE N'INVENTE AUCUNE API. N'utilise que les fonctions, événements et champs listés ci-dessous (ou dans mods/sdk/modding.d.ts si l'auteur te le fournit). Si la demande exige quelque chose que l'API ne propose pas, DIS-LE clairement ("l'API des mods ne permet pas X") et propose l'alternative réalisable la plus proche ; ne devine pas un nom de fonction, ne simule pas l'API manquante. Écris du code en anglais (identifiants, commentaires de code) ; réponds dans la langue de l'auteur. ## Ce qu'est un mod - Un dossier avec `main.ts` (point d'entrée), SANS manifeste JSON. Une seule déclaration : `export default defineMod({...})` au premier niveau. - Tourne dans un moteur QuickJS isolé sur un thread `mods` (pas de DOM, pas de réseau, pas de fichiers, pas de timers, pas d'import()). Imports relatifs permis dans le dossier du mod seulement. Transpilé SANS vérification de types. - Ajouter en tête : `/// ` (les fonctions sont des GLOBALES typées, pas des imports). - Trois genres : mod (`defineMod`, main.ts), skin (`defineSkin`, skin.ts), script de map (`defineChart`, script.ts). Ne mélange pas leurs API. - Budgets : 16 Mio, 1 Mio de pile, 30 ms par chargement/setup/événement. 3 échecs de suite ou 3 dépassements => mod désactivé. Garde les gestionnaires courts ; pas de boucle longue ; pas de travail par image. ## Squelette ```ts /// export default defineMod({ id: "mon-mod", // a-z 0-9 . - _ ; 1..64 ; commence et finit par lettre/chiffre ; stable name: "Mon mod", // 1..64 version: "1.0.0", // semver apiVersion: 2, // EXACTEMENT 2 // optionnels : author, description, homepage, fonts, uses, provides, permissions, images, loadOrder (0..10000) setup() { /* register* + ctx.on + hud.* */ }, }); ``` Champ inconnu dans defineMod => erreur. `setup` ne reçoit pas `ctx` : `ctx` est un global ; ne nomme jamais un paramètre `ctx`. ## Phases (qui peut appeler quoi) - Chargement (premier niveau) : seulement defineMod, log.*, ctx.on. Tout le reste lève "is not available while the mod loads". - setup : tout, et SEUL moment des `*.register` (judgements, scrollSpeed, ratings, tables, library.filters, controls, gameplay, tabs.register/extend, downloads.register/registerBridge, skinImport.register). - événements : game.*, hud.*, stage.*, storage.*, metron.*, ratings.backfill, leaderboard.*, ui.*, skinImport.* (hors register). - hud.* et storage.* : mods et skins, PAS les scripts de map. playfield.* et lanes.set : skins et scripts de map, PAS les mods. - Trois permissions existent, déclarées dans `permissions: [...]` : "stage" (stage.*), "network" (downloads.*), "skinImport" (skinImport.*). `network` ne donne AUCUN accès réseau au script : le jeu fait toutes les requêtes. ## Unités - Temps de chanson : microsecondes (`timeUs`, `offsetUs`, `durationUs`). Exception : `Hit.offsetMs` et fenêtres de jugement en ms. - HUD/stage : `x`,`y` = fractions du parent (écran au premier niveau, (0,0) en haut à gauche). `width`,`height`, `size`, `radius`, `padding`, `gap`... = fractions de la HAUTEUR de l'écran (0.05 = 54 px en 1080p). Couleurs `#rgb` `#rgba` `#rrggbb` `#rrggbbaa`. - Anchor : topLeft top topRight left center right bottomLeft bottom bottomRight. ## Événements : ctx.on("nom", handler) game.songStart {song, judgements:{preset,tiers[]}, timeUs} | game.judgement {column,timeUs,offsetUs,tier:{index,id,name,color},combo,counts[],accuracy} game.pause/game.resume {timeUs} | game.tick {timeUs,hitCount} (30 Hz, en jeu et hors pause) | game.beat {index,timeUs,bpm,meterBeat} game.songEnd {counts[],maxCombo,accuracy,aborted} | game.playfieldChange Playfield | game.settingsChange ModSettings elements.configure {element,from,options} | controls.action {id,pressed,timeUs} | library.ready {} | library.chartAdd {chartId} library.chartsAdded {chartIds[],truncated} | ratings.progress {requestId,id,done,total,failed,finished,error?,ahead} metron.performanceResult {requestId,calculator,value?,unit,error?} | ui.action {id} Les événements passent par une file bornée : un événement peut être perdu. Ne bâtis pas d'état critique dessus. ## Lectures (setup ou événement) game.song(): Song|null {title,artist,creator,difficulty,mode,layout,keys,durationUs,noteCount,holdCount,bpm?,timing[],ratings} game.notes({fromUs,toUs,column?,cursor?,limit?<=256}): {notes:[{index,column,timeUs,endUs?}], next?} (pagine avec next) game.hits({limit?<=256}): [{offsetMs,tier}] (derniers impacts non-Miss, ancien -> récent) game.playfield(): {keys,lanes:[{column,x,width}],hitY,spawnY,scrollTimeUs}|null game.player(): {playing,paused,timeUs,combo,maxCombo,counts,judged,accuracy} game.judgements(): {preset,tiers:[TierInfo]}|null ; game.settings(): {volume,showFps,scrollTimeMs,audioOffsetMs,ratingSystem}|null TierInfo {index,id,name,color,gradient?,earlyMs?,lateMs?,weight?,breaksCombo} (Miss : earlyMs/lateMs nuls ; Wife3 : weight nul) ## HUD hud.text({id,parent?,text,bind?,visible?,showWhen?,animate?,element?,layout?,style?}) | hud.box({...,fill?}) | hud.image({...,src,fit?}) hud.group({...,flow?}) | hud.update({id, ...champs}) | hud.remove(id) | hud.clear() - Réutiliser un id remplace le nœud (même parent). Limites par mod : 256 nœuds, 256 caractères, 8 niveaux, id 1..64. - PRÉFÈRE les liaisons à un handler par jugement : bind {kind}: combo maxCombo hits misses judged remaining accuracy{decimals} tierCount{tier} tierName{tier} lastJudgement{colors?} elapsed total timer fps label{label} action{action:"skipIntro"} ghostAccuracy ghostDelta ghostCombo ghostName. fill (box): songProgress | accuracy | tierShare{tier}. showWhen: paused running showFps judged ghost. animate: {on: judgement|miss|hit, kind: pop|popFade|flash, durationMs 1..5000}. - layout {x,y,width,height,anchor} ; style {color,background,gradient{angle,stops},border{width,color},radius,padding,opacity,font,size,weight,italic,align,shadow,transform,transitionMs}. - Un mod ne crée des nœuds que pendant une partie (songStart) et les retire à songEnd (`hud.clear()`). ## Stage natif (permission "stage") stage.sprite|rect|text|emitter|trigger({id,...}) ; stage.play/stop({target,animation}) ; stage.remove(id) ; stage.clear(). at {space: screen|playfield|lane|receptor, column?, x?, y?} ; layer: below|lanes|above. Animations et déclencheurs sont DÉCLARÉS une fois, le rendu les joue : pas de boucle par image. Limites : 128 éléments, 16 émetteurs, 1024 particules, 64 déclencheurs. Images : `images: [...]` du mod. ## Enregistrements (setup seulement) judgements.register({id,name,params?,accuracy,holds,tiers(params)}) : accuracy osuScoreV1|wife3|weights|continuous ; holds osuCombined|etterna|head|separate. Paliers du plus serré au plus large, Miss en dernier. Tier {id,name,color,windowMs | earlyMs+lateMs, weight?, breaksCombo?, gradient?}. tiers() est PURE et SYNCHRONE, appelée une fois par combinaison de params (min..max par step). 2..33 paliers. id : a-z 0-9 - (32 max). scrollSpeed.register({id,name,param:{label,min,max,step,default,short?},toMs:(value,{travel})=>ms}) : ms positif et fini. gameplay.register({id,name(32),description(200),kind,group?,icon?,conflictsWith?}) : kind auto|ghost|mirror|random|noLn|fullLn UNIQUEMENT (comportement natif ; un script ne peut pas créer un modificateur ni injecter des touches). controls.register({id,name,defaultKey:"KeyH"}) -> événement controls.action ; defaultKey = code physique accepté (KeyA, Digit1, Backquote...). tables.register({id,columns:[{name,kind:"number"|"text",indexed?}]}) ; ratings.register({id,name,calculator,unit,table,column,version,panels?}) ; library.filters.register({id,name,table,column,version,unit?}) : table déclarée par CE mod ; version = metron.catalog().calculators[i].version. panels (max 4) : metrics|bars|radar|timeline ; ratings.backfill({id,charts?}) -> id de demande -> ratings.progress. metron.catalog() ; metron.difficulty({calculator}) ; metron.performance({calculator,accuracy 0..1}) -> metron.performanceResult (osu-2018 pp, etterna-515 SSR). ui.popup({title,detail,done,total,actions?}) ; ui.dismiss(). Limites par mod : 8 jeux, 8 vitesses, 8 modificateurs, 16 tables, 16 vues, 32 filtres, 32 actions. ## Classement, onglets, téléchargements, import de skin leaderboard.query({chartId,limit?<=100,offset?}) / leaderboard.best({chartId}) -> id de demande | null ; réponse : événement leaderboard.result {requestId,chartId,offset,total,entries:[{rank,replayId,playerName?,received,accuracy,performance?,maxCombo,misses,tiers,rate,modified,playedAtMs}],judgement,performance?,unavailableReplays,error?}. chartId = id de la bibliothèque (library.chartAdd / library.chartsAdded) ; game.song() n'a PAS de chartId. tabs.register({id,title(24),icon?,order?,panels(1..8)}) ; tabs.extend({tab:"info"|"leaderboard"|"mods",slot:"top"|"bottom",order?,panels(1..4)}) : setup seulement. panels : metrics|bars|radar (fields[{label,source,unit?,decimals?}]) | timeline | leaderboard{title,limit 1..10} | text{title?,text<=280}. source : {kind:"column",rating,column} (vue ratings.register du MÊME mod) | {kind:"chart",metric} | {kind:"leaderboard",stat:"plays"|"bestPerformance"|"bestAccuracy"}. downloads.register({id,name,kind:"mirror"|"source",target?:"osu",hosts[1..8],rateLimit?,auth?,search?,download:{url|urlField}}) : DONNÉES seulement (permission network, setup). downloads.registerBridge({id,name,hosts,auth?,search(query,page,state),onResponse?(response,state),action?(result,actionId,state)}) : fonctions PURES qui renvoient une étape {request|results|download|error, state?, nextPage?} ; l'hôte fait les requêtes (6 max par recherche). Une exception désactive le pont. skinImport.register({id,name,sources:["folder"|"archive"]}) ; skinImport.stage({sourceId,skin,images}) ; skinImport.create({stageId}) ; skinImport.panel({...}) ; skinImport.close() ; événements skinImport.opened/staged/created/action (permission skinImport). Exemple complet : mods/skin-converter. ## Stockage et journal storage.get(key) | storage.set({key,value}) | storage.remove(key) | storage.keys() | storage.clear() : JSON, 256 Kio, 256 clés. log.info(text) | log.warn(text). ## Dépendances et éléments (patron des mods de HUD livrés) uses: { "autre.mod": "^1" } ou { version, required?, feature? } ; ctx.has("autre.mod") ; ctx.element("autre.mod/element").configure({...options}). provides: { elements: { nom: { root?: "idDuNoeudRacine", options: { x:{type:"number",min,max,default,label,player?}, ... } } } } types d'option : number integer boolean string color enum colors. Le fournisseur reçoit `elements.configure` et reconstruit son HUD. ## Skin (skin.ts) et script de map (script.ts) defineSkin({id,name,version,apiVersion:2,uses?,images?,permissions?,setup(play)}) ; defineChart({apiVersion:2,images?,permissions?,setup(play)}) (pas d'id). play: PlayContext {mode,layout,layoutSkin,columns,screenWidth,screenHeight,song,judgements}. playfield.set(spec) ; playfield.update({patch,transitionMs?,easing?}) easing linear|easeIn|easeOut|easeInOut ; lanes.set({column,lane,transitionMs?,easing?}). Un script de map est visuel seulement (pas de HUD, pas de storage). Les transitions sont animées par le rendu ; ne les pilote pas à chaque image : démarre-les sur game.beat. ## INTERDITS (n'existent pas : ne les utilise pas) DOM, document, window, fetch, XMLHttpRequest, WebSocket, require, import dynamique, import hors du dossier, fs, setTimeout/setInterval, console.log (utilise log.info), lecture/écriture de fichiers, son/audio, lecture ou écriture des réglages du joueur (sauf game.settings() en lecture), injection de touches ou de jugements, lecture de la sélection, du classement d'un autre joueur ou en ligne (leaderboard.* est le classement LOCAL), requête réseau écrite dans le script (même avec "network"), HTML/CSS brut, shaders, polices autres que .ttf/.otf, images autres que .png, modifier les notes, créer un nouveau genre de modificateur, appeler un autre mod directement (passe par provides/ctx.element), insérer des lignes dans une table (aucune API : seul le calcul natif Metron les remplit), connaître le chartId de la partie en cours, la vitesse (rate) de la partie, les modificateurs actifs ou le score total (non exposés par game.*). `Math.random` et `Date` : disponibilité non vérifiée ; évite-les. ## Pièges fréquents 1. `apiVersion` autre que 2 => le mod est refusé. 2. defineMod appelé dans setup ou deux fois => échec (même si l'exception est attrapée). 3. register* hors de setup => erreur. hud.* au premier niveau du fichier => erreur. 4. Tailles en pixels au lieu de fractions de la hauteur de l'écran => nœuds énormes ou hors bornes (TypeError). 5. Mettre à jour un nœud à chaque game.judgement alors qu'un `bind` suffit => travail inutile, budget 30 ms. 6. Oublier de retirer le HUD à game.songEnd ; oublier hud.clear() avant de reconstruire. 7. tiers() non pure, async ou avec effets de bord ; paliers non croissants ; Miss pas en dernier ; `weight` absent (sauf accuracy wife3). 8. ids de jeu/palier/action : seulement a-z 0-9 - ; l'id du mod autorise aussi . et _. 9. `ctx.element(...)` sans `uses` du paquet => exception. Toujours tester `ctx.has(...)`. 10. Un nœud d'un autre type : `hud.update` avec `text` sur un `box` => exception. 11. `stage.*` sans permissions: ["stage"] => erreur. 12. Compter sur la réception de CHAQUE événement (file bornée). ## CHECKLIST (à suivre avant de répondre) [ ] Chaque fonction/événement/champ utilisé figure dans ce fichier ou dans modding.d.ts ; sinon je le dis. [ ] `apiVersion: 2`, id valide, version semver, un seul defineMod au premier niveau, `export default`. [ ] Les register* sont dans setup ; rien d'interdit au chargement. [ ] Unités : temps en µs, tailles en hauteurs d'écran, couleurs hex valides. [ ] Le HUD est créé à songStart et nettoyé à songEnd ; liaisons plutôt que handlers par jugement. [ ] Aucun accès DOM/réseau/fichier/timers ; aucun import hors du dossier. [ ] Le code passe `tsc --strict` avec la référence au SDK (je l'indique si je n'ai pas pu le vérifier). [ ] J'indique où placer le dossier (%LOCALAPPDATA%\Prism\PrismNG\data\mods\\main.ts) et de cliquer sur Reload dans la page Mods. ## Recettes (voir docs/modding/cookbook.md pour le code complet) 1 jugement (judgements.register) | 2 vitesse (scrollSpeed.register) | 3 HUD par liaisons | 4 hit bar (game.hits + game.tick) | 5 élément configurable (provides.elements) | 6 modificateur | 7 touche (controls.register) | 8 table + vue + filtre | 9 stage (étincelles) | 10 stockage | 11 skin | 12 script de map | 13 lecteur de classement | 14 onglet de sélection | 15 source de téléchargement déclarative | 16 pont d'API | 17 import de skin (squelette). ## MODÈLE DE PROMPT (l'auteur le colle après ce fichier) """ Tu écris un mod pour le jeu Prism en suivant STRICTEMENT le contexte ci-dessus. N'invente aucune API ; si ce que je demande n'est pas possible avec l'API listée, dis-le et propose une alternative. Objectif du mod : Genre : Ce que je veux voir/configurer : Rends : (1) le contenu complet de main.ts, (2) la liste des fichiers du dossier, (3) comment l'installer et le tester, (4) la liste de ce que tu n'as pas pu faire car l'API ne l'offre pas, (5) la checklist cochée. """