Aller au contenu

Module:Pokémon distribué/bac à sable

De Poképédia

La documentation pour ce module peut être créée à Module:Pokémon distribué/bac à sable/doc

--[==[
     Module:Pokémon distribué — la fiche d'une distribution.

     Rend du wikitexte, repassé par `frame:preprocess`.
     Cas d'école : `module-tests.lua`, à lancer avec `lua`.
     Le pourquoi de chaque garde, et les défauts déjà payés :
     `wiki/README.md`.

     Le plan de la fiche. Six colonnes, cinq quand il n'y a pas de ruban.
     Chaque ligne du tableau a sa fonction, nommée à droite.

       col. 1      col. 2-3        col. 4-5            col. 6
     ┌─────────┬───────────────┬───────────────────┬─────────┐
     │         │ N. 10         │ Capacités         │ Ruban   │ ligne_entetes
     │         ├───────────────┼─────────┬─────────┼─────────┤
     │ sprite  │ types         │ cap. 1  │ type    │ ruban   │ ligne_types
     │ (4 l.)  │ Téra/Gigamax  │ cap. 2  │ type    │ (4 l.,  │ ligne_tera_gigamax
     │         ├──────┬────────┼─────────┼─────────┤  ou 2+2 │
     │         │ DO : │ valeur │ cap. 3  │ type    │  avec   │ ligne_do
     ├─────────┼──────┼────────┼─────────┼─────────┤  deux)  │
     │Ball nom │ ID : │ valeur │ cap. 4  │ type    │         │ ligne_nom_et_id
     ├─────────┴──────┴──┬─────┴─────────┴──┬──────┴─────────┤
     │ Objet tenu :      │ Talent :         │ Nature :       │ lignes_objet_talent_nature
     │ valeur            │ valeur           │ valeur         │
     ├───────────────────┴──────────────────┴────────────────┤
     │ une ligne pleine par donnée présente : origine,       │ lignes_du_bas
     │ distribution, versions, Carte Cadeau, IV et EV,       │
     │ rencontre, puis les bandeaux explicatifs              │
     └───────────────────────────────────────────────────────┘

     Sans Téracristal ni Gigamax, la case des types couvre deux lignes.
     ]==]

local p = {}

-- LES COULEURS DE LA FICHE, reprises de l'ancien modèle en wikitexte.
local FOND_ENTETE    = "#ddf"
local FOND_ETIQUETTE = "#eef"
local FOND_VALEUR    = "#f4f4fc"
local FOND_CAPACITE  = "aliceblue"

local COULEUR_MALE    = "DodgerBlue"
local COULEUR_FEMELLE = "red"

-- Les coins arrondis d'une cellule. Une étiquette et sa valeur forment
-- une paire : l'étiquette est arrondie à gauche ou en haut, la valeur à
-- droite ou en bas.
local ARRONDI_A_GAUCHE = "5px 0 0 5px"
local ARRONDI_A_DROITE = "0 5px 5px 0"
local ARRONDI_EN_HAUT  = "5px 5px 0 0"
local ARRONDI_EN_BAS   = "0 0 5px 5px"

local TIRET = "—"
local NOUVELLE_LIGNE = '\n|- align="center"'
local CATEGORIE_DE_MAINTENANCE =
	"[[Catégorie:Page avec un paramètre de distribution incorrect]]"

--- Tous les paramètres que le modèle comprend. Un autre nom est une faute
-- de frappe, ou un reste d'une version ancienne : la page va alors dans
-- la catégorie de maintenance.
local PARAMETRES_CONNUS = {}
for _, nom in ipairs({
	"jeu", "num", "taille", "forme", "pokemon", "nom", "nomvarie",
	"type", "type2", "type-téra", "gigamax",
	"sexe", "sexediff", "chromatique", "baron", "oeuf", "niveau", "nature",
	"talent", "talent2", "talent3", "talentcaché",
	"do", "genre-do", "traduction-do", "dovarie", "id",
	"ball", "objet", "objet2", "objet-transfert", "ruban", "taille-ruban", "ruban2", "taille-ruban2",
	"origine", "date", "date-debut", "date-fin", "canal", "versions",
	"carte-titre", "carte-numero", "evenement", "region", "pays",
	"distribution", "langue",
	"iv", "iv-pv", "iv-att", "iv-def", "iv-attspe", "iv-defspe", "iv-vit", "iv-special", "ev",
	"lieu-rencontre", "fatidique",
	-- Ajoutés le 2026-09-26 (4ᵉ génération) : la rencontre et l'Œuf, les
	-- notes du jeu, le jeu d'où vient le Pokémon, la Carte Miracle, la lettre.
	"date-rencontre", "niveau-rencontre", "lieu-oeuf", "date-oeuf", "date-selon",
	"coreen", "beaute-max", "langue-origine", "oublier", "oublier-depuis", "oublier-par", "ball-varie",
	"obtenu-depuis",
	"carte-langue", "carte-icones", "carte-message", "carte-titre-fr", "carte-message-fr",
	"lettre-message", "lettre-traduction",
	-- 5ᵉ génération : la description de la Carte Miracle.
	"carte-description", "carte-description-fr",
}) do
	PARAMETRES_CONNUS[nom] = true
end
for i = 1, 4 do
	for _, prefixe in ipairs({ "cap-nom", "cap-type", "cap-cat", "cap-even" }) do
		PARAMETRES_CONNUS[prefixe .. i] = true
	end
end

--- Les valeurs admises pour `region`, `pays` et `canal`. Une valeur hors
-- liste range la page dans la catégorie de maintenance : une requête ne
-- retrouve une donnée que si elle est toujours écrite de la même façon.
-- La casse ne compte pas ; la fiche enregistre l'écriture de la liste.
-- ⚠️ Listes de départ, à corriger au fil du chantier.
local function liste(valeurs)
	local t = {}
	for _, v in ipairs(valeurs) do t[mw.ustring.lower(v)] = v end
	return t
end

-- La grande région : elle choisit la section de la page.
local REGIONS = liste({
	"Monde", "Japon", "Amérique du Nord", "Amérique latine", "Europe",
	"Australie", "Corée du Sud", "Taïwan", "Chine", "Hong Kong", "Asie du Sud-Est",
})

-- Le pays, quand la distribution ne couvrait pas toute la région.
local PAYS = liste({
	"Japon", "États-Unis", "Canada", "Mexique", "Brésil",
	"Royaume-Uni", "Irlande", "France", "Allemagne", "Espagne", "Italie",
	"Belgique", "Pays-Bas", "Luxembourg", "Suisse", "Autriche", "Portugal",
	"Danemark", "Norvège", "Suède", "Finlande", "Grèce", "République tchèque",
	"Pologne", "Australie", "Nouvelle-Zélande", "Corée du Sud", "Taïwan",
	"Chine", "Hong Kong", "Singapour", "Malaisie", "Thaïlande", "Philippines",
	"Indonésie", "Porto Rico", "Turquie", "Colombie", "Chili",
	"Guatemala", "Venezuela", "Pérou", "Panama", "Costa Rica", "Afrique du Sud", "Russie",
})

-- Le moyen technique de la distribution. Chaque valeur traduit une ou
-- plusieurs écritures de Bulbapedia (bas de fiche `entrybottom`, ou titre
-- de page en 4ᵉ et 5ᵉ génération) :
--   Distribution locale  local, local wireless, Local Communication
--   Wi-Fi                pages « Wi-Fi … » (4ᵉ et 5ᵉ génération)
--   Nintendo Network     Nintendo Network (6ᵉ et 7ᵉ)
--   Internet             Internet (8ᵉ et plus)
--   Code de série        serial code, Serial Code (A/J/K/U codes)
--   Mot de passe         password, Password:
--   Échange              Y-Comm trade, Global Room trade
--   Récompense de compétition  Battle Stadium Rewards, Official Online Competition
-- « Cadeau Mystère » n'est pas un canal : c'est le menu du jeu qui reçoit
-- presque toutes les distributions.
local CANAUX = liste({
	"Distribution locale", "Wi-Fi", "Nintendo Network", "Internet",
	"Code de série", "Mot de passe", "Infrarouge", "Nintendo Zone",
	"Pokémon Link", "Code QR", "Poké Ball Plus", "GTS", "Échange",
	"Récompense de compétition", "Pokémon Global Link", "Pokémon Dream World",
	"Pokéwalker", "Pokémon HOME", "En jeu",
})

-- La langue de la liste qui porte la fiche (une par copie, comme le tableau
-- {{eventAvail}} de Bulbapedia sur ses pages de Pokémon).
local LANGUES = liste({
	"japonais", "anglais", "français", "allemand", "italien", "espagnol",
	"coréen", "chinois",
})

-- OUTILS

--- La valeur d'un paramètre, sans espaces autour. "" quand il est absent.
local function val(args, nom)
	local v = args[nom]
	if v == nil then return "" end
	v = mw.text.trim(v)
	-- ⚠️ Un saut de ligne suivi de « | » ou « ! » ouvre une CELLULE ou une
	-- LIGNE de tableau. Aucune fiche n'en contient aujourd'hui : la garde
	-- est préventive, mais une seule valeur de ce genre casserait la fiche
	-- et les fiches suivantes de la page.
	return (v:gsub("\n%s*([|!])", " %1"))
end

--- Un paramètre vaut-il `attendu` ? La casse ne compte pas.
local function vaut(args, nom, attendu)
	return mw.ustring.lower(val(args, nom)) == mw.ustring.lower(attendu)
end

--- Un paramètre vaut-il « oui » ? La casse ne compte pas.
-- Toute autre valeur vaut « non », y compris `non` : 20 fiches écrivent
-- `nomvarie=non` et ne veulent pas du bandeau.
local function oui(args, nom)
	return vaut(args, nom, "oui")
end

--- La valeur, ou un tiret quand elle est vide.
local function ou_tiret(v)
	return v ~= "" and v or TIRET
end

--- Joint les morceaux avec `separateur`, en sautant les morceaux vides.
local function joindre_non_vides(morceaux, separateur)
	local gardes = {}
	for _, m in ipairs(morceaux) do
		if m ~= "" then table.insert(gardes, m) end
	end
	return table.concat(gardes, separateur)
end

--- Les valeurs d'un paramètre à plusieurs valeurs, séparées par des
-- virgules (`pays = Danemark, Norvège, Suède`), ramenées à l'écriture de
-- la liste `admises`. Rend les valeurs reconnues, puis `true` si une
-- valeur est hors liste.
local function valeurs_admises(args, nom, admises)
	local reconnues, inconnue = {}, false
	for brute in mw.text.gsplit(val(args, nom), ",", true) do
		local v = mw.text.trim(brute)
		if v ~= "" then
			local ecrite = admises[mw.ustring.lower(v)]
			if ecrite then
				table.insert(reconnues, ecrite)
			else
				inconnue = true
			end
		end
	end
	return reconnues, inconnue
end

--- Le texte sans ses liens : `[[Miaouss Gigamax]]` devient
-- `Miaouss Gigamax`, `[[Pikachu|Pika]]` devient `Pika`.
local function sans_liens(s)
	return (s:gsub("%[%[[^%]|]*|([^%]]*)%]%]", "%1"):gsub("%[%[([^%]]*)%]%]", "%1"))
end

--- La première lettre en capitale : `mémoire` devient `Mémoire`.
local function capitale(s)
	return mw.ustring.upper(mw.ustring.sub(s, 1, 1)) .. mw.ustring.sub(s, 2)
end

-- LA MISE EN FORME

--- Les coins arrondis, en CSS. `rayon` vaut 5px par défaut.
local function arrondi(rayon)
	return "border-radius:" .. (rayon or "5px") .. ";"
end

--- Une cellule du tableau, précédée de son saut de ligne.
-- `c` est une table :
--   c[1]      le contenu de la cellule
--   fond      la couleur de fond ; sans fond, la cellule n'est pas arrondie
--   arrondi   le rayon des coins, 5px par défaut
--   css       du CSS en plus, par exemple "font-size:85%;"
--   colspan, rowspan
local function cellule(c)
	local attributs = ""
	if c.colspan then attributs = attributs .. 'colspan="' .. c.colspan .. '" ' end
	if c.rowspan then attributs = attributs .. 'rowspan="' .. c.rowspan .. '" ' end
	local css = c.css or ""
	if c.fond then
		css = "background:" .. c.fond .. "; " .. arrondi(c.arrondi)
			.. (css ~= "" and (" " .. css) or "")
	end
	if css ~= "" then attributs = attributs .. 'style="' .. css .. '" ' end
	return "\n| " .. attributs .. (attributs ~= "" and "| " or "") .. c[1]
end

--- Un lien écrit en noir, pour les étiquettes.
local function lien_noir(page, texte)
	return "[[" .. page .. '|<span style="color:black">' .. texte .. "</span>]]"
end

--- L'étiquette d'une donnée : petite, grasse, liée à sa page d'aide.
local function etiquette(page, texte)
	return "<small>'''" .. lien_noir(page, texte) .. "'''</small>"
end

--- Le texte en gras quand `actif` est vrai.
-- ⚠️ Le gras passe par `<strong>`, pas seulement par le CSS : un lecteur
-- d'écran ignore `font-weight`, et les bandeaux du bas promettent « en
-- gras » à tous les lecteurs.
local function gras(texte, actif)
	return actif and ("<strong>" .. texte .. "</strong>") or texte
end

--- La graisse de la cellule, en CSS. Elle double `gras()` pour l'œil.
local function css_gras(actif)
	return "font-weight:" .. (actif and "bold" or "normal") .. ";"
end

-- LE SPRITE ET LE NOM

--- Les écritures acceptées pour le sexe, ramenées à m, f ou mf.
-- Les autres valeurs (« Au hasard », `{{?}}`) n'affichent rien : on ne
-- connaît pas toujours le sexe du Pokémon distribué.
local SEXES = {
	m = "m", f = "f", mf = "mf",
	["♂"] = "m", ["♀"] = "f", ["m/f"] = "mf", ["♂/♀"] = "mf",
}

local function sexe(args)
	return SEXES[mw.ustring.lower(val(args, "sexe"))] or ""
end

--- L'image du Pokémon. Le nom du fichier se compose morceau par morceau :
--   Sprite_0025_Alola_♀_chromatique_XY.png
--   numéro, forme, sexe (avec `sexediff`), chromatique, jeu
-- `num` porte parfois un suffixe, comme `0025 ♂` ou `0423 Orient`.
local function sprite(args)
	local nom = "Sprite_" .. val(args, "num")
	local forme = val(args, "forme")
	if forme ~= "" then nom = nom .. "_" .. forme end
	-- `sexediff` : l'espèce a une apparence différente selon le sexe.
	-- ⚠️ `oui()` et non `~= ""` : `sexediff=non` doit dire NON.
	if oui(args, "sexediff") then
		nom = nom .. (sexe(args) == "f" and "_♀" or "_♂")
	end
	if oui(args, "chromatique") then nom = nom .. "_chromatique" end
	nom = nom .. "_" .. val(args, "jeu") .. ".png"

	local taille = val(args, "taille")
	-- ⚠️ Sans `alt`, un lecteur d'écran annonce le nom du fichier.
	local espece = val(args, "pokemon")
	return "[[Fichier:" .. nom .. (taille ~= "" and ("|" .. taille) or "")
		.. (espece ~= "" and ("|alt=" .. espece) or "") .. "]]"
end

local function symbole_sexe(args)
	local male = '<span style="color:' .. COULEUR_MALE .. '">♂</span>'
	local femelle = '<span style="color:' .. COULEUR_FEMELLE .. '">♀</span>'
	local s = sexe(args)
	if s == "m" then return " " .. male end
	if s == "f" then return " " .. femelle end
	if s == "mf" then return " " .. male .. "/" .. femelle end
	return ""
end

--- La Ball, le nom du Pokémon, son sexe et ses marques.
-- `pokemon` est l'espèce, et sert de lien. `nom` est le nom porté à la
-- distribution : un nom étranger, ou en capitales avant la 5ᵉ génération.
local function texte_nom(args)
	-- 124 fiches écrivent la Ball en minuscules : `capitale()` les accepte.
	local ball = val(args, "ball")
	if ball == "" then ball = "Poké" end
	-- ⚠️ Pokémon Or : les sprites portent le code « O », les miniatures
	-- d'objet le code « OA » (Or et Argent). La casse ne compte pas.
	local jeu = val(args, "jeu")
	if mw.ustring.upper(jeu) == "O" then jeu = "OA" end
	local espece = val(args, "pokemon")
	local nom = val(args, "nom")
	if nom == "" then nom = espece end

	local texte = "{{Objet|" .. capitale(ball) .. " Ball|nom=non|jeu=" .. jeu
		.. "}} '''[[" .. espece .. "|" .. nom .. "]]'''" .. symbole_sexe(args)
	if oui(args, "chromatique") then
		texte = texte .. ' [[Pokémon chromatique|<span style="color:'
			.. COULEUR_FEMELLE .. '">★</span>]]'
	elseif vaut(args, "chromatique", "possible") then
		-- Les Œufs mystère de la 2ᵉ génération : le Pokémon PEUT éclore
		-- chromatique (Bulbapedia : `shiny=maybe`). Étoile creuse.
		-- `1=` et `2=` sont nommés : sans eux, le « = » de `style=` fait de
		-- « <span style » un nom de paramètre, et l'infobulle affiche {{{2}}}.
		texte = texte .. ' {{Infobulle|1=<span style="color:' .. COULEUR_FEMELLE
			.. '">☆</span>|2=Peut être chromatique}}'
	end
	-- Un Pokémon Baron vient de Légendes Pokémon : Arceus (code LPA).
	if oui(args, "baron") then
		texte = texte .. " [[Fichier:Icône Pokémon Baron LPA.png|20px|link=Pokémon Baron"
			.. "|Pokémon Baron]]"
	end
	return texte
end

--- Le niveau, et la marque de l'Œuf. Un Œuf a quand même un niveau :
-- 5 ou 1 selon le jeu.
local function texte_niveau(args)
	local texte = etiquette("Niveau", "N.") .. " " .. ou_tiret(val(args, "niveau"))
	if oui(args, "oeuf") then
		texte = texte .. " {{Objet|Œuf|nom=non}}"
			.. "{{infobulle|*|Ce Pokémon a été distribué sous forme d'Œuf.}}"
	end
	return texte
end

-- LES CAPACITÉS

--- La capacité est-elle « au hasard » plutôt qu'un nom ?
-- ⚠️ L'orthographe sans accent est acceptée : sinon, lien rouge.
local function est_aleatoire(nom)
	local n = mw.ustring.lower(nom)
	return n == "aléatoire" or n == "aleatoire"
end

--- Le type et la catégorie d'une capacité, ou un tiret.
-- `{{type|…}}` dessine aussi la catégorie : il reconnaît `physique`,
-- `spécial` et `statut`. ⚠️ `spéciale`, au féminin, rend un fichier absent.
local function type_de_capacite(args, i)
	local t = val(args, "cap-type" .. i)
	if t == "" then return TIRET end
	local texte = "{{type|" .. t .. "}}"
	local cat = val(args, "cap-cat" .. i)
	if cat ~= "" then texte = texte .. "&nbsp;{{type|" .. cat .. "}}" end
	return texte
end

--- Les deux cellules de la capacité n° i : son nom, puis son type.
-- Une capacité exceptionnelle (`cap-even`) est une capacité que l'espèce
-- n'apprend pas normalement, comme un Pikachu qui connaît Surf.
-- Elle s'affiche en gras, et un bandeau l'explique en bas de la fiche.
local function cellules_capacite(args, i)
	local nom = val(args, "cap-nom" .. i)
	if nom == "" then
		return cellule({ TIRET, colspan = 2 })
	end
	local texte, droite
	if est_aleatoire(nom) then
		texte, droite = "Aléatoire", TIRET
	else
		texte, droite = "[[" .. nom .. "]]", type_de_capacite(args, i)
	end
	local exceptionnelle = oui(args, "cap-even" .. i)
	-- La hauteur fixe garde les quatre lignes de capacités égales.
	return cellule({ gras(texte, exceptionnelle), fond = FOND_CAPACITE,
			css = "height:28px; " .. css_gras(exceptionnelle) })
		.. "\n| " .. droite
end

-- LE DRESSEUR D'ORIGINE (DO) ET L'IDENTIFIANT (ID)

--- Le DO est-il celui du joueur qui reçoit le Pokémon ? `do=joueur`.
local function do_est_le_joueur(args)
	return vaut(args, "do", "joueur")
end

--- La couleur du DO dit le sexe du dresseur, comme dans les jeux.
-- `genre-do` ne sert plus à partir de la 6ᵉ génération.
local function couleur_du_do(args)
	local g = mw.ustring.lower(val(args, "genre-do"))
	if g == "m" then return COULEUR_MALE end
	if g == "f" then return COULEUR_FEMELLE end
	return "Black"
end

--- Ce qu'on affiche quand personne ne connaît le DO ou l'ID.
local INCONNU = "{{Infobulle|''Inconnu''|Cette information n'est pas connue.}}"

--- Une valeur dit-elle « on ne sait pas » ? `inconnu` (l'écriture
-- conseillée), mais aussi ce que les sources écrivent : `Unknown`, `???`,
-- `?????`, et `{{?}}`, qui arrive ici déjà rendu (« ?? » dans une balise).
-- ⚠️ Enregistrée telle quelle, « ?? » déclenchait l'avertissement de
-- Semantic MediaWiki sous la fiche (constaté le 2026-09-25).
local function est_inconnu(v)
	-- Les parenthèses gardent la seule chaîne : `gsub` rend aussi un compte,
	-- que `trim` prendrait pour son jeu de caractères.
	local nu = mw.text.trim(((v or ""):gsub("<[^>]*>", "")))
	local bas = mw.ustring.lower(nu)
	return bas == "inconnu" or bas == "unknown" or (nu ~= "" and nu:match("^[%?%s]+$") ~= nil)
end

--- Les DO d'une distribution, séparés par des virgules : un même Mew
-- porte `EUROPE, SWEDEN, NORWAY, FINLAND, DENMARK` selon le pays.
-- ⚠️ Des fiches écrites à la main séparent le dernier par « ou » :
-- `FLAGSB, FLAGSD, FLAGSA, FLAGSC ou FLAGSE`.
-- Un DO inconnu n'est pas un DO : il n'entre pas dans la liste.
local function les_do(args)
	local liste = {}
	local texte = val(args, "do"):gsub("%s+ou%s+", ",")
	for brut in mw.text.gsplit(texte, ",", true) do
		local v = mw.text.trim(brut)
		if v ~= "" and not est_inconnu(v) then table.insert(liste, v) end
	end
	return liste
end

local function texte_do(args)
	if do_est_le_joueur(args) then
		return "{{Infobulle|''Joueur''|Le DO et l'ID sont ceux du joueur "
			.. "ayant obtenu le Pokémon.}}"
	end
	if est_inconnu(val(args, "do")) then return INCONNU end
	-- Plusieurs DO : un par ligne, comme Bulbapedia.
	local texte = ou_tiret(table.concat(les_do(args), "<br>"))
	-- Un DO en caractères japonais ou coréens porte sa traduction en infobulle.
	local traduction = val(args, "traduction-do")
	if traduction ~= "" then
		texte = "{{infobulle|" .. texte .. "|" .. traduction .. "}}"
	end
	return '<span style="color:' .. couleur_du_do(args) .. '">' .. texte .. "</span>"
end

local function texte_id(args)
	-- ⚠️ Si le DO est le joueur, l'ID l'est aussi : on ne l'affiche pas.
	if do_est_le_joueur(args) then return TIRET end
	if est_inconnu(val(args, "id")) then return INCONNU end
	return ou_tiret(val(args, "id"))
end

-- L'OBJET ET LE TALENT

--- Un objet en lien. « - » veut dire « aucun objet », et reste « - ».
local function lien_objet(nom)
	return (nom == "-") and "-" or ("[[" .. nom .. "]]")
end

--- Les codes de sprite de la 1ʳᵉ génération : seuls jeux où un objet
-- n'existe qu'au transfert.
local JEUX_DE_LA_PREMIERE_GENERATION = { rv = true, rb = true, j = true, b = true, r = true, v = true }

--- L'objet n'apparaît-il qu'au transfert vers la 2ᵉ génération ?
-- La 1ʳᵉ génération n'a pas d'objet tenu. Mais l'octet du taux de capture
-- devient l'objet tenu quand Or, Argent ou Cristal reçoivent le Pokémon par
-- la Capsule Temporelle, et la machine de distribution (ou Pokémon Stadium)
-- y écrivait une valeur choisie : le Coffre Normal ou le Coffre Luxe de
-- Stadium, la Baie du Magicarpe de Tamamushi. Cet objet fait donc partie de
-- la distribution. Aucun autre passage de génération ne fonctionne ainsi.
local function objet_au_transfert(args)
	return oui(args, "objet-transfert")
end

local function est_de_la_premiere_generation(args)
	return JEUX_DE_LA_PREMIERE_GENERATION[mw.ustring.lower(val(args, "jeu"))] == true
end

local function texte_objet(args)
	-- ⚠️ `objet2` n'est PAS un second objet tenu : c'est l'objet
	-- ALTERNATIF, quand il dépendait de la version ou de la vague.
	local texte = lien_objet(val(args, "objet"))
	local objet2 = val(args, "objet2")
	if objet2 ~= "" then
		texte = texte .. " <small>ou</small> " .. lien_objet(objet2)
	end
	if objet_au_transfert(args) then
		texte = texte .. "{{Infobulle|<sup>*</sup>|Seulement après un transfert "
			.. "vers la deuxième génération, par la Capsule Temporelle.}}"
	end
	return texte
end

--- Les talents possibles, séparés par « ou ».
-- « - » veut dire « pas de talent » : les talents n'existent pas en
-- 1ʳᵉ et 2ᵉ génération.
--- Les codes de sprite des deux premières générations : ni talent ni
-- nature n'y existent (ils apparaissent en 3ᵉ génération).
local JEUX_SANS_TALENT_NI_NATURE = {
	rv = true, rb = true, j = true, b = true, r = true, v = true,
	o = true, oa = true, a = true, c = true, oac = true,
}
local AVANT_LA_TROISIEME = "{{Infobulle|—|N'existe pas avant la troisième génération.}}"

--- La fiche vient-elle d'un jeu sans talent ni nature ? Alors « — », et
-- jamais « Au hasard » : la nature n'existait pas (décidé le 2026-09-25).
local function sans_talent_ni_nature(args)
	return JEUX_SANS_TALENT_NI_NATURE[mw.ustring.lower(val(args, "jeu"))] == true
end

local function texte_talent(args)
	if sans_talent_ni_nature(args) then return AVANT_LA_TROISIEME end
	local talents = {}
	for _, champ in ipairs({ "talent", "talent2", "talent3" }) do
		local v = val(args, champ)
		if v == "-" then
			table.insert(talents, TIRET)
		elseif v ~= "" then
			table.insert(talents, "[[" .. v .. "]]")
		end
	end
	if #talents == 0 then return TIRET end
	return table.concat(talents, " <small>ou</small> ")
end

-- LE RUBAN

--- Un champ de ruban porte-t-il vraiment un ruban ?
-- ⚠️ « aucun » est une valeur, pas un vide. Écarter les DEUX champs.
local function porte_un_ruban(args, champ)
	local r = mw.ustring.lower(val(args, champ))
	return r ~= "" and r ~= "aucun"
end

--- La cellule d'un ruban : son image, puis son nom.
-- Le nom du ruban est écrit sous l'image : l'image est donc décorative
-- (`alt=` vide), et un lecteur d'écran ne lit le nom qu'une fois.
local function cellule_ruban(nom, taille, rowspan)
	local image = "[[Fichier:Ruban " .. nom .. ".png"
		.. (taille ~= "" and ("|" .. taille) or "") .. "|alt=|link=]]"
	return cellule({ image .. "<br/>[[Ruban " .. nom .. "]]", rowspan = rowspan,
		fond = FOND_CAPACITE, css = "font-size:85%; padding:4px;" })
end

-- LES LIGNES DU TABLEAU, DE HAUT EN BAS
-- Chaque fonction reçoit la fiche `f` (voir `decrire`) et rend sa ligne.

--- L'ancre de la fiche, pour qu'un lien `[[Page#Mew Aura]]` y mène.
-- Elle vient de `evenement`, pas du titre de section : elle survit donc
-- à un titre renommé et à une fiche déplacée d'une page à l'autre.
-- MediaWiki remplace lui-même les espaces par des soulignés, comme pour
-- un titre de section.
local function ancre(f)
	if f.evenement == "" then return "" end
	return '<span id="' .. f.evenement:gsub('"', "&quot;") .. '"></span>'
end

local function ouverture(f)
	-- ⚠️ Le cadre qui défile évite le débordement sur écran étroit, et
	-- il porte la marge : à l'intérieur, elle mangerait de la course.
	-- Le cadre prend la couleur du premier type.
	local type1 = val(f.args, "type")
	return ancre(f)
		.. '<div style="overflow-x:auto; max-width:100%; margin:0 0 0.5em 2em;">'
		.. '\n{| style="border:3px double {{Couleur|'
		.. (type1 ~= "" and type1 or "inconnu") .. '}}; ' .. arrondi() .. '"'
end

--- Ligne 1 : le sprite (sur 4 lignes), le niveau, les en-têtes.
local function ligne_entetes(f)
	local texte = NOUVELLE_LIGNE
		.. cellule({ sprite(f.args), rowspan = 4, css = "width:150px;" })
		.. cellule({ texte_niveau(f.args), colspan = 2, css = "width:120px;" })
		.. cellule({ "'''" .. lien_noir("Capacité", "Capacités") .. "'''", colspan = 2,
			fond = FOND_ENTETE, css = "width:200px; height:25px; font-size:90%;" })
	if f.a_ruban then
		texte = texte .. cellule({ "'''" .. lien_noir("Ruban (série principale)",
			"Ruban" .. (f.a_ruban2 and "s" or "")) .. "'''",
			fond = FOND_ENTETE, css = "font-size:90%;" })
	end
	return texte
end

--- Ligne 2 : les types, la capacité 1, le (premier) ruban.
local function ligne_types(f)
	local type1, type2 = val(f.args, "type"), val(f.args, "type2")
	local texte = NOUVELLE_LIGNE
	if f.a_tera or f.a_gigamax then
		-- Sur une seule ligne : les deux types côte à côte.
		texte = texte .. cellule({ "{{type|" .. type1 .. "}}"
			.. (type2 ~= "" and ("{{type|" .. type2 .. "}}") or ""), colspan = 2 })
	else
		-- Sur deux lignes : `{{type|A|B}}` empile les deux types.
		texte = texte .. cellule({ "{{type|" .. type1
			.. (type2 ~= "" and ("|" .. type2) or "") .. "}}", colspan = 2, rowspan = 2 })
	end
	texte = texte .. cellules_capacite(f.args, 1)
	if f.a_ruban then
		-- Seul, le ruban couvre les lignes 2 à 5. Avec un second ruban,
		-- chacun couvre deux lignes.
		texte = texte .. cellule_ruban(val(f.args, "ruban"), val(f.args, "taille-ruban"),
			f.a_ruban2 and 2 or 4)
	end
	return texte
end

--- Ligne 3 : le type Téracristal ou le Gigamax, la capacité 2.
-- Sans l'un ni l'autre, la case des types de la ligne 2 descend ici.
local function ligne_tera_gigamax(f)
	local texte = NOUVELLE_LIGNE
	if f.a_tera then
		texte = texte .. cellule({ "<small>Type [[Téracristallisation|Téracristal]] :"
			.. "</small> {{type|" .. val(f.args, "type-téra") .. "|téra=oui}}", colspan = 2 })
	elseif f.a_gigamax then
		texte = texte .. cellule({ "[[Fichier:Miniature Gigamax EB.png|25px|"
			.. "link=Gigamax|Capable de gigamaxer]] [[Gigamax]]", colspan = 2 })
	end
	return texte .. cellules_capacite(f.args, 2)
end

--- Ligne 4 : le DO, la capacité 3, le second ruban.
local function ligne_do(f)
	local texte = NOUVELLE_LIGNE
		.. cellule({ etiquette("DO", "DO :"), fond = FOND_ETIQUETTE, arrondi = ARRONDI_A_GAUCHE })
		.. cellule({ texte_do(f.args), fond = FOND_VALEUR, arrondi = ARRONDI_A_DROITE })
		.. cellules_capacite(f.args, 3)
	if f.a_ruban2 then
		texte = texte .. cellule_ruban(val(f.args, "ruban2"), val(f.args, "taille-ruban2"), 2)
	end
	return texte
end

--- Ligne 5 : la Ball et le nom (sous le sprite), l'ID, la capacité 4.
local function ligne_nom_et_id(f)
	return NOUVELLE_LIGNE
		.. cellule({ texte_nom(f.args), fond = FOND_VALEUR, css = "font-size:90%;" })
		.. cellule({ etiquette("ID", "ID :"), fond = FOND_ETIQUETTE, arrondi = ARRONDI_A_GAUCHE })
		.. cellule({ texte_id(f.args), fond = FOND_VALEUR, arrondi = ARRONDI_A_DROITE })
		.. cellules_capacite(f.args, 4)
end

--- Lignes 6 et 7 : les étiquettes, puis les valeurs de l'objet tenu, du
-- talent et de la nature. Sans objet, le talent et la nature se partagent
-- toute la largeur.
local function lignes_objet_talent_nature(f)
	local cases = f.a_objet and 2 or 3
	local function entete(page, texte, colspan)
		return cellule({ etiquette(page, texte), colspan = colspan,
			fond = FOND_ETIQUETTE, arrondi = ARRONDI_EN_HAUT })
	end
	local function valeur(texte, colspan, css)
		return cellule({ texte, colspan = colspan, fond = FOND_VALEUR,
			arrondi = ARRONDI_EN_BAS, css = "font-size:85%;" .. (css or "") })
	end

	local etiquettes, valeurs = NOUVELLE_LIGNE, NOUVELLE_LIGNE
	if f.a_objet then
		etiquettes = etiquettes .. entete("Objet tenu", "Objet tenu :", 2)
		valeurs = valeurs .. valeur(texte_objet(f.args), 2)
	end
	local cache = oui(f.args, "talentcaché")
	local nature = val(f.args, "nature")
	etiquettes = etiquettes .. entete("Talent", "Talent :", cases)
		.. entete("Nature", "Nature :", cases)
	valeurs = valeurs
		.. valeur(gras(texte_talent(f.args), cache), cases, " " .. css_gras(cache))
		.. valeur(sans_talent_ni_nature(f.args) and AVANT_LA_TROISIEME
			or (nature ~= "" and nature or "Au hasard"), cases)
	return etiquettes .. valeurs
end

-- LES LIGNES PLEINE LARGEUR, EN BAS DE LA FICHE
-- Chaque fonction `texte_*` rend le contenu de sa ligne, ou "" pour
-- l'omettre.

--- Le séparateur entre deux données d'une même ligne. L'espace insécable
-- colle le point à la donnée qui le précède : une ligne coupée ne
-- commence jamais par « · ».
local SEPARATEUR = "&nbsp;· "

local function texte_origine(args)
	local origine = val(args, "origine")
	if origine == "" then return "" end
	-- Une valeur telle que HOME l'écrit : « Marque de Galar », « Pentagone bleu ».
	return etiquette("Origine", "Origine :") .. " [[Fichier:Icône " .. origine
		.. " HOME.png|25px]] " .. origine
end

--- Une date écrite avec la précision qu'on connaît : « 2004 »,
-- « juillet 2004 » ou « 17 juillet 2004 ». Rend aussi la précision.
-- ⚠️ `{{#time:}}` lit « 2004 » seul comme l'heure 20 h 04 : une année ne
-- lui est jamais passée. Une valeur d'une autre forme lui est passée
-- telle quelle, pour qu'il affiche son erreur en rouge : une date fausse
-- qui s'affiche quand même serait pire qu'une date absente.
-- `j xg Y` écrit le mois au génitif (« 17 juillet »), `F Y` au nominatif.
local function date_lisible(v)
	if v:match("^%d%d%d%d$") then
		return v, "annee"
	elseif v:match("^%d%d%d%d%-%d%d$") then
		return "{{#time:F Y|" .. v .. "-01}}", "mois"
	end
	return "{{#time:j xg Y|" .. v .. "}}", "jour"
end

--- La date affichée. Trois paramètres, trois rôles :
--   date-debut  le premier jour : 2004, 2004-07 ou 2004-07-17
--   date-fin    le dernier jour, sous les mêmes formes
--   date        le texte libre, pour une période avec des coupures
--               (« du 30 août au 26 septembre 2002, puis du 1er au
--               7 novembre ») ou une date floue (« Été 1996 »)
-- Le texte libre, quand il existe, gagne à l'affichage. Les deux bornes
-- restent alors des données, pour trier et chercher.
local function texte_de_date(args)
	local libre = val(args, "date")
	if libre ~= "" then return libre end
	local debut, fin = val(args, "date-debut"), val(args, "date-fin")
	if debut == "" then return "" end
	local d, precision = date_lisible(debut)
	if fin == "" then
		if precision == "jour" then return "Le " .. d end
		return "En " .. d
	end
	local f, precision_fin = date_lisible(fin)
	if precision == "jour" and precision_fin == "jour" then
		return "Du " .. d .. " au " .. f
	end
	return "De " .. d .. " à " .. f
end

--- La date et le canal : Wi-Fi, code de série…
local function texte_distribution(args)
	local date = texte_de_date(args)
	local canal = table.concat(valeurs_admises(args, "canal", CANAUX), ", ")
	if date == "" and canal == "" then return "" end
	return etiquette("Événement Nintendo", "Distribution :") .. " "
		.. joindre_non_vides({ date, canal }, "&nbsp;&mdash;&nbsp;")
end

--- Les jeux où la distribution était recevable. Pas de lien : la page
-- n'existe pas.
local function texte_versions(args)
	local versions = val(args, "versions")
	if versions == "" then return "" end
	return "<small>'''Recevable dans :'''</small> " .. versions
end

--- La Carte Cadeau : la carte que le jeu affiche quand le joueur reçoit
-- le Pokémon. Son titre en français, et son numéro.
local function texte_carte_cadeau(args)
	local titre, numero = val(args, "carte-titre"), val(args, "carte-numero")
	if titre == "" and numero == "" then return "" end
	-- Une Carte Miracle avec son message a sa propre boîte (boite_carte).
	if val(args, "carte-message") ~= "" and numero == "" then return "" end
	return etiquette("Cadeau Mystère", "Carte Cadeau :") .. " "
		.. joindre_non_vides({
			titre ~= "" and ("''" .. titre .. "''") or "",
			numero ~= "" and ("<small>(n°&nbsp;" .. numero .. ")</small>") or "",
		}, "&nbsp;")
end

--- Les six IV, dans l'ordre du jeu : le paramètre, puis son libellé.
local SIX_IV = {
	{ "iv-pv", "PV" }, { "iv-att", "Att." }, { "iv-def", "Déf." },
	{ "iv-attspe", "Att.&nbsp;Spé." }, { "iv-defspe", "Déf.&nbsp;Spé." },
	{ "iv-vit", "Vit." },
}

--- Les cinq IV des deux premières générations, de 0 à 15 : l'Attaque
-- spéciale et la Défense spéciale n'y font qu'une, le Spécial.
local CINQ_IV = {
	{ "iv-pv", "PV" }, { "iv-att", "Att." }, { "iv-def", "Déf." },
	{ "iv-vit", "Vit." }, { "iv-special", "Spécial" },
}

--- Les IV : un par statistique quand on les connaît, sinon la phrase libre.
-- ⚠️ « 3 IV à 31 » ne dit pas LESQUELS. Les deux formes coexistent donc.
-- `iv-special` rempli fait passer la fiche aux cinq IV des 1ʳᵉ et 2ᵉ générations.
local function texte_iv(args)
	local morceaux, detaille = {}, false
	for _, iv in ipairs(val(args, "iv-special") ~= "" and CINQ_IV or SIX_IV) do
		local v = val(args, iv[1])
		if v ~= "" then detaille = true end
		table.insert(morceaux, iv[2] .. "&nbsp;" .. ou_tiret(v))
	end
	local texte = detaille and table.concat(morceaux, SEPARATEUR) or val(args, "iv")
	if texte == "" then return "" end
	return etiquette("IV", "IV garantis :") .. " " .. texte
end

local function texte_statistiques(args)
	local ev = val(args, "ev")
	return joindre_non_vides({
		texte_iv(args),
		ev ~= "" and (etiquette("EV", "EV :") .. " " .. ev) or "",
	}, SEPARATEUR)
end

--- Le jeu d'où vient le Pokémon, quand il n'est pas reçu par une
-- distribution : Pokémon Ranger, Pokémon Battle Revolution…
local function texte_obtenu_depuis(args)
	local jeux = val(args, "obtenu-depuis")
	if jeux == "" then return "" end
	return "<small>'''Obtenu depuis :'''</small> " .. jeux
end

--- Une date de rencontre ou d'Œuf, au jour près : « le 17 mars 2009 ».
local function le_jour(v)
	if v == "" then return "" end
	return "le " .. (date_lisible(v))
end

local function texte_rencontre(args)
	local lieu = val(args, "lieu-rencontre")
	local niveau = val(args, "niveau-rencontre")
	if lieu ~= "" and niveau ~= "" then
		lieu = lieu .. " (niveau&nbsp;" .. niveau .. ")"
	end
	local date = val(args, "date-rencontre")
	if lieu ~= "" and date ~= "" then
		lieu = lieu .. ", " .. le_jour(date)
	elseif date ~= "" then
		lieu = le_jour(date)
	end
	local oeuf = joindre_non_vides({ val(args, "lieu-oeuf"), le_jour(val(args, "date-oeuf")) }, ", ")
	return joindre_non_vides({
		lieu ~= "" and ("<small>'''Rencontre :'''</small> " .. lieu) or "",
		oeuf ~= "" and ("<small>'''Œuf reçu :'''</small> " .. oeuf) or "",
		oui(args, "fatidique") and ("{{Infobulle|rencontre fatidique|Ce Pokémon "
			.. "porte le marqueur de rencontre fatidique, exigé par certains "
			.. "événements en jeu.}}") or "",
	}, SEPARATEUR)
end

--- Les bandeaux explicatifs du bas de la fiche, dans l'ordre.
local function bandeaux(args)
	local out = {}

	for i = 1, 4 do
		if oui(args, "cap-even" .. i) then
			table.insert(out, "Une capacité en '''gras''' est normalement "
				.. "inaccessible à ce Pokémon, selon son espèce ou son niveau.")
			break
		end
	end
	if oui(args, "talentcaché") then
		table.insert(out, "Un talent en '''gras''' est un talent caché, "
			.. "uniquement accessible par certaines méthodes.")
	end

	-- Plusieurs DO suffisent à dire que le DO varie ; `dovarie` reste
	-- accepté pour les fiches qui n'en écrivent qu'un.
	local nom_varie = oui(args, "nomvarie")
	local do_varie = oui(args, "dovarie") or #les_do(args) > 1
	local sujet
	if nom_varie and do_varie then
		sujet = "Le nom et le [[Dresseur d'Origine|DO]] de ce Pokémon varient"
	elseif nom_varie then
		sujet = "Le nom de ce Pokémon varie"
	elseif do_varie then
		sujet = "Le [[Dresseur d'Origine|DO]] de ce Pokémon varie"
	end
	if sujet then
		table.insert(out, sujet .. " en fonction de la langue du jeu "
			.. "dans lequel il a été obtenu.")
	end

	-- Les notes que le jeu impose, reprises du modèle de Bulbapedia
	-- (4ᵉ génération, 2026-09-26).
	local selon = val(args, "date-selon")
	if selon == "boutique" then
		table.insert(out, "La date de rencontre est celle de la console le jour où le Pokémon "
			.. "a été récupéré auprès du livreur de la [[Boutique Pokémon]].")
	elseif selon == "eclosion" then
		table.insert(out, "Ce Pokémon a été distribué dans un Œuf : sa date de rencontre est "
			.. "celle de son éclosion, et diffère pour chaque Pokémon.")
	end
	if oui(args, "ball-varie") then
		table.insert(out, "La Ball de ce Pokémon dépend de celle utilisée pour le capturer.")
	end
	local langue_origine = val(args, "langue-origine")
	if langue_origine ~= "" then
		-- « l'anglais », « le japonais » : l'article suit la première lettre.
		local article = langue_origine:match("^[aeiouyéèêAEIOUY]") and "l'" or "le "
		table.insert(out, "La langue d'origine de ce Pokémon est " .. article .. langue_origine
			.. ", quelle que soit la langue du jeu qui l'a reçu.")
	end
	if oui(args, "coreen") then
		table.insert(out, "Les caractères coréens n'apparaissent pas dans les jeux non coréens.")
	end
	if oui(args, "beaute-max") then
		table.insert(out, "La Beauté et le Lustre de ce Pokémon sont au maximum.")
	end
	local oublier = val(args, "oublier")
	if oublier ~= "" then
		local depuis = val(args, "oublier-depuis")
		-- 3ᵉ génération : le Parc des Amis ; 4ᵉ génération : le Poké Transfert.
		local par = val(args, "oublier-par")
		table.insert(out, "Ce Pokémon doit oublier la capacité [[" .. oublier .. "]] pour être "
			.. "envoyé par le " .. (par ~= "" and par or "[[Poké Transfert]]")
			.. (depuis ~= "" and (" depuis " .. depuis) or "") .. ".")
	end
	return out
end

-- LA CARTE MIRACLE ET LA LETTRE, À CÔTÉ DE LA FICHE

local STYLE_BOITE = "display:inline-block; vertical-align:top; margin:0 0 1em 2em; "
	.. "max-width:300px; text-align:left; font-size:90%;"

--- Une boîte à la manière de Bulbapedia : un en-tête, puis des lignes.
local function boite(entete, lignes, fond_entete, fond)
	-- ⚠️ `{|` et `|}` doivent commencer une ligne : collés au `<div>`, le
	-- tableau s'affichait en texte brut (Raikou Crown, 2026-09-26).
	local out = { '\n<div style="' .. STYLE_BOITE .. '">\n',
		'{| style="width:300px; background:' .. fond .. '; border:2px solid #485060; border-radius:8px;"',
		'\n! style="background:' .. fond_entete .. '; border-radius:6px;" | ' .. entete }
	for _, ligne in ipairs(lignes) do
		if ligne ~= "" then
			table.insert(out, '\n|-\n| style="background:#f8f8ff; border-radius:6px;" | ' .. ligne)
		end
	end
	table.insert(out, "\n|}\n</div>")
	return table.concat(out)
end

--- Les icônes de la carte : `carte-icones = 0251, 0492` → deux miniatures.
local function icones(args)
	local out = {}
	for numero in val(args, "carte-icones"):gmatch("[^,%s]+") do
		table.insert(out, "{{miniature|" .. numero .. "}}")
	end
	return table.concat(out, " ")
end

--- La Carte Miracle que le joueur reçoit avec le Pokémon : son titre et son
-- message dans la langue de la carte, puis leur traduction.
-- En 5ᵉ génération, la carte a aussi une description (`carte-description`),
-- le texte que le jeu montre en la lisant.
local function boite_carte(args)
	local message, description = val(args, "carte-message"), val(args, "carte-description")
	if message == "" and description == "" then return "" end
	local langue = val(args, "carte-langue")
	local entete = "[[Carte Miracle]]" .. (langue ~= "" and (" <small>(en " .. langue .. ")</small>") or "")
		.. " " .. icones(args)
	local titre_fr, message_fr = val(args, "carte-titre-fr"), val(args, "carte-message-fr")
	local description_fr = val(args, "carte-description-fr")
	local traduction = ""
	if titre_fr ~= "" or message_fr ~= "" or description_fr ~= "" then
		traduction = "<small>'''Traduction :''' "
			.. joindre_non_vides({ titre_fr ~= "" and ("''" .. titre_fr .. "''") or "", message_fr,
				description_fr }, "<br>")
			.. "</small>"
	end
	local titre = val(args, "carte-titre")
	return boite(entete, { titre ~= "" and ("'''" .. titre .. "'''") or "", message, description, traduction },
		"#d0b0f8", "#c0e0f8")
end

--- La lettre que tient le Pokémon, et sa traduction.
local function boite_lettre(args)
	local message = val(args, "lettre-message")
	if message == "" then return "" end
	local traduction = val(args, "lettre-traduction")
	local objet = val(args, "objet")
	return boite("Lettre" .. (objet ~= "" and (" <small>(" .. objet .. ")</small>") or ""),
		{ message, traduction ~= "" and ("<small>'''Traduction :''' " .. traduction .. "</small>") or "" },
		"#f8e0a0", "#fff8e0")
end

--- Les lignes pleine largeur, dans l'ordre. Une donnée absente n'a pas
-- de ligne.
-- ⚠️ La taille du texte a trois états : 85 % pour les données, 75 % pour
-- les bandeaux, la taille normale pour l'origine et la distribution.
local function lignes_du_bas(f)
	local function pleine(contenu, fond, taille, rayon)
		if contenu == "" then return "" end
		return NOUVELLE_LIGNE .. cellule({ contenu, colspan = f.largeur, fond = fond,
			arrondi = rayon, css = taille and ("font-size:" .. taille .. ";") })
	end
	local a = f.args
	local out = {
		pleine(texte_origine(a), FOND_ETIQUETTE, nil, ARRONDI_EN_HAUT),
		pleine(texte_distribution(a), FOND_ETIQUETTE),
		pleine(texte_versions(a), FOND_VALEUR, "85%"),
		pleine(texte_obtenu_depuis(a), FOND_VALEUR, "85%"),
		pleine(texte_carte_cadeau(a), FOND_VALEUR, "85%"),
		pleine(texte_statistiques(a), FOND_VALEUR, "85%"),
		pleine(texte_rencontre(a), FOND_VALEUR, "85%"),
	}
	for _, bandeau in ipairs(bandeaux(a)) do
		table.insert(out, pleine(bandeau, FOND_ENTETE, "75%"))
	end
	return table.concat(out)
end

-- LES DONNÉES INTERROGEABLES, INVISIBLES POUR LE LECTEUR

--- Range les données de la fiche dans Semantic MediaWiki, installé sur
-- Poképédia. Une requête `{{#ask:}}`, sur une autre page, peut ensuite
-- les rassembler : par exemple, toutes les distributions de Mew.
-- Chaque propriété est déclarée par une page `Attribut:Distribution …`.
local function donnees_semantiques(f)
	local args = f.args
	local morceaux = {}
	local function poser(nom, valeur)
		-- ⚠️ Une valeur balisée n'est pas une donnée, et sa barre
		-- verticale couperait la liste de paires.
		if valeur ~= "" and not valeur:find("[{}|%[%]]") then
			table.insert(morceaux, nom .. "=" .. valeur)
		end
	end
	poser("Distribution espèce", val(args, "pokemon"))
	poser("Distribution jeu", val(args, "jeu"))
	for _, v in ipairs(les_do(args)) do
		poser("Distribution DO", v)
	end
	-- ⚠️ Si le DO est le joueur, l'ID est le sien : l'exposer
	-- donnerait une fausse clé de rapprochement.
	poser("Distribution ID", (do_est_le_joueur(args) or est_inconnu(val(args, "id"))) and "" or val(args, "id"))
	poser("Distribution niveau", val(args, "niveau"))
	poser("Distribution début", val(args, "date-debut"))
	poser("Distribution fin", val(args, "date-fin"))
	poser("Distribution carte cadeau", val(args, "carte-numero"))
	poser("Distribution événement", f.evenement)
	-- La clé commune à toutes les copies d'une même distribution
	-- (`Mew-Aura-2007`) : la page du Pokémon regroupe les copies par elle.
	poser("Distribution clé", val(args, "distribution"))
	-- Une propriété à plusieurs valeurs se répète : `Distribution pays=
	-- Norvège|Distribution pays=Suède`. Une requête « Norvège » la trouve.
	for _, champ in ipairs({ { "Distribution région", "region", REGIONS },
			{ "Distribution pays", "pays", PAYS }, { "Distribution canal", "canal", CANAUX },
			{ "Distribution langue", "langue", LANGUES } }) do
		for _, v in ipairs((valeurs_admises(args, champ[2], champ[3]))) do
			poser(champ[1], v)
		end
	end
	if #morceaux == 0 then return "" end
	-- Un ordre fixe, pour une sortie stable.
	table.sort(morceaux)
	-- ⚠️ `#subobject` et non `#set` : `#set` attache les valeurs à la PAGE,
	-- et les 20 à 120 fiches d'une page de liste se mélangeraient. Le
	-- premier paramètre vide donne à chaque fiche son propre paquet.
	return "{{#subobject:|" .. table.concat(morceaux, "|") .. "}}"
end

-- LA GARDE DE MAINTENANCE

--- Un paramètre porte-t-il un nom que le modèle ne comprend pas ?
-- `nomvaire` (faute de frappe), `Ball` (majuscule), `img_ball` (ancien
-- paramètre), `1` (une barre verticale en trop dans la saisie)…
local function a_un_parametre_inconnu(args)
	-- Scribunto permet `pairs()` sur ses arguments, mais pas `next()`.
	for nom in pairs(args) do
		if not PARAMETRES_CONNUS[nom] then return true end
	end
	return false
end

--- Une valeur de `region`, `pays`, `canal` ou `langue` est-elle hors de sa liste ?
local function a_une_valeur_hors_liste(args)
	for champ, admises in pairs({ region = REGIONS, pays = PAYS, canal = CANAUX, langue = LANGUES }) do
		if select(2, valeurs_admises(args, champ, admises)) then return true end
	end
	return false
end

--- Range la page parmi celles à réparer, quand la saisie se contredit,
-- contient un paramètre inconnu ou une valeur hors liste.
local function categorie_de_maintenance(args)
	local fautes = {
		a_une_valeur_hors_liste(args),
		-- une fiche sans clé : ajoutée à la main, elle compterait comme une
		-- distribution de plus sur la page du Pokémon
		val(args, "distribution") == "",
		-- une taille de ruban sans le ruban qu'elle habille
		val(args, "taille-ruban") ~= "" and not porte_un_ruban(args, "ruban"),
		val(args, "taille-ruban2") ~= "" and not porte_un_ruban(args, "ruban2"),
		-- un second ruban sans le premier
		porte_un_ruban(args, "ruban2") and not porte_un_ruban(args, "ruban"),
		-- Téracristal et Gigamax ensemble : le module n'en rend qu'un
		val(args, "type-téra") ~= "" and oui(args, "gigamax"),
		-- un objet « au transfert » hors de la 1ʳᵉ génération, ou sans objet :
		-- le mécanisme n'existe qu'entre la 1ʳᵉ et la 2ᵉ génération
		objet_au_transfert(args) and (not est_de_la_premiere_generation(args) or val(args, "objet") == ""),
		a_un_parametre_inconnu(args),
	}
	for _, faute in ipairs(fautes) do
		if faute then return CATEGORIE_DE_MAINTENANCE end
	end
	return ""
end

-- L'ASSEMBLAGE

--- Les arguments de l'appel : ceux du modèle, ou ceux de `#invoke`.
-- Aucune page n'appelle `#invoke` directement : le second cas sert aux
-- essais.
local function arguments(frame)
	-- ⚠️ Jamais de `next()` sur ces arguments : Scribunto les bâtit par
	-- métatable, et `next()` y rend toujours nil.
	-- ⚠️ `getParent()` rend nil sur un appel direct depuis une page.
	local parent = frame:getParent()
	if parent == nil then return frame.args end
	if parent.args.pokemon == nil and frame.args.pokemon ~= nil then
		return frame.args
	end
	return parent.args
end

--- Ce que la fiche contient, calculé une fois pour toutes les lignes.
local function decrire(args)
	local a_ruban = porte_un_ruban(args, "ruban")
	return {
		args = args,
		a_tera = val(args, "type-téra") ~= "",
		a_gigamax = oui(args, "gigamax"),
		a_objet = val(args, "objet") ~= "",
		a_ruban = a_ruban,
		-- ⚠️ Un second ruban n'existe que si le premier existe : sinon, une
		-- cellule sans en-tête décale tout le tableau.
		a_ruban2 = a_ruban and porte_un_ruban(args, "ruban2"),
		largeur = a_ruban and 6 or 5,
		-- Le nom de la distribution, sans ses liens : il sert d'ancre et
		-- de donnée (« Mew Aura »).
		evenement = sans_liens(val(args, "evenement")),
	}
end

function p.fiche(frame)
	local f = decrire(arguments(frame))
	local out = {
		ouverture(f),
		ligne_entetes(f),
		ligne_types(f),
		ligne_tera_gigamax(f),
		ligne_do(f),
		ligne_nom_et_id(f),
		lignes_objet_talent_nature(f),
		lignes_du_bas(f),
		"\n|}\n</div>",
		boite_carte(f.args),
		boite_lettre(f.args),
		donnees_semantiques(f),
		categorie_de_maintenance(f.args),
	}
	-- ⚠️ `preprocess` est indispensable : la sortie d'un module est
	-- insérée APRÈS l'expansion, donc nos {{…}} resteraient en clair.
	return frame:preprocess(table.concat(out))
end

return p