Companion Dogs : manuel de modding
Comment construire votre propre add-on : une nouvelle race, ou une espèce entière, sous la forme d'un mod Workshop séparé qui se branche sur Companion Dogs.
Ce manuel s'adresse à ceux qui écrivent des mods. Si vous voulez seulement jouer, lisez les deux autres manuels : Fonctionnalités pour ce que fait le chien, et Races pour les races.
Il suppose que vous savez lire le Lua et que vous avez un modèle riggé et animé pour votre animal. Le pipeline artistique n'est pas traité ici. La section 2 liste ce que le fichier de modèle doit respecter.
Tout ce que ce manuel nomme est un contrat public et existe pour être appelé depuis l'extérieur du mod. Ce qui n'est pas nommé ici est interne et peut changer à n'importe quelle version, sans préavis.
1. Ce qu'est un add-on
Un add-on est un mod Workshop ordinaire qui déclare require=CompanionDogs et appelle une fonction au chargement. Il ajoute une race (ou plusieurs, si elles partagent un corps) et rien d'autre.
Le mod de base possède :
- la machine à états d'animation, l'animset et le squelette
- le suivi, le pathing, le combat, la chasse, la conduite de troupeau, la sentinelle, les besoins et l'entretien
- toute l'interface : la fenêtre du chien, le radial, le menu contextuel, le chenil, le marqueur sur la carte
- l'apprivoisement, le lien, la reproduction, les chiots et les croisements
- les options du bac à sable et la réplication multijoueur
Votre add-on possède :
- les chiffres qui rendent votre animal différent d'un Caramelo
- son modèle de corps, sa texture, son portrait et, s'il en a une, son icône de moodle
- l'endroit où on le trouve dans le monde
- son nom et sa description, dans chaque langue que vous livrez
Deux couleurs qui se comportent pareil restent deux races pour le mod de base (deux noms, deux entrées dans le chenil) ; vous les écrivez à partir d'une table commune, comme CD: Cats le fait avec ses cinq pelages.
2. Ce qu'il vous faut avant de commencer
Votre animal a besoin de son propre .glb skinné sous media/models_X/Skinned/, avec le jeu complet des 21 clips d'animation Rac_*. Les clips ne s'héritent pas entre fichiers de modèle : un modèle qui en a quinze vous donne un animal qui se fige la première fois qu'on lui en demande un des six autres. Le modèle doit aussi rester sous les 60 os et avoir son nœud racine à l'identité, sinon le moteur lève une exception à chaque frame et l'animal s'affiche en traînée noire. Deux clips de plus sont optionnels, Rac_WalkLimpFront et Rac_WalkLimpBack (cycles de marche boiteuse) ; ils ne comptent que si vous mettez limpAnim = true dans la définition de race.
Si vous n'avez pas encore de modèle, vous pouvez écrire et tester tout le reste en pointant CD.applyDogModel sur l'un des corps du mod de base. L'animal ressemble à un Caramelo, mais tous les systèmes de ce manuel tournent.
Il vous faut aussi une texture pour le corps et un portrait pour la fenêtre du chenil.
Décidez si votre animal est une race ou une espèce avant d'écrire la moindre ligne de code. Une race est un chien avec d'autres chiffres. Une espèce est un animal à qui certains métiers manquent tout à fait. Le choix change les champs que vous écrivez ; la section 6 s'en occupe.
3. Les fichiers d'un add-on
Voici CD: Pug, le plus petit add-on publié, avec les parties qui comptent :
CompanionDogsPug/
42/
mod.info
icon.png
poster.png
media/
models_X/Skinned/Pug_Body.glb
scripts/models_pug.txt
textures/Body/Pug.png
textures/CDPortrait_pug.png
lua/shared/CompanionDogsPug_Breed.lua
lua/shared/Definitions/animal/PugDefinitions.lua
lua/shared/Definitions/animal/CompanionDogsPug_Parts.lua
lua/client/CompanionDogsPug_Moodle.lua
lua/shared/Translate/EN/IG_UI.json
lua/shared/Translate/PTBR/IG_UI.json
common/
Et le mod.info :
id=CompanionDogsPug
name=CD: Pug
description=Pugs for Companion Dogs. REQUIRES the Companion Dogs mod (0.6.8 or newer).
poster=poster.png
icon=icon.png
require=CompanionDogs
modversion=0.1.0
pzversion=42.19.0
versionMin=42.18.0
require=CompanionDogs est la ligne importante. Elle garantit que tout le Lua du mod de base a tourné avant le vôtre, à chaque phase : shared d'abord, puis client, puis server. Sans elle, vos fichiers peuvent charger en premier et chaque appel CD. devient un index sur nil.
versionMin ne sait pas dire "il faut Companion Dogs 0.6.8". Ce champ ne filtre que la build du jeu, et il n'existe aucun champ de mod.info pour la version d'une dépendance. Le plancher sur le mod de base est tenu par la garde de la section suivante et annoncé aux joueurs dans votre description. Écrivez-le là, en toutes lettres, ou le seul symptôme que reçoit le joueur est un animal qui n'apparaît jamais.
4. La garde de version
Mettez ceci en haut de chaque fichier Lua de votre add-on, avec le numéro dont votre add-on a besoin :
local CD = CompanionDogs
if not (CD and CD.registerBreed and (CD.API_VERSION or 0) >= 3) then return end
Avec le mod de base absent ou trop vieux, le fichier retourne à la ligne deux et l'add-on ne fait rien : pas de race, pas de spawn, pas de moodle, pas de ligne de log.
La garde est par fichier, et chaque fichier garde sur ce qu'il utilise. PugDefinitions.lua a besoin des helpers de modèle, donc il garde sur eux :
if not (CompanionDogs and CompanionDogs.applyDogModel and CompanionDogs.DOG_SOUNDS) then return end
Ne créez pas le global. CompanionDogs = CompanionDogs or {} dans un add-on transforme un mod de base absent en table à moitié construite, et toutes les gardes en aval passent alors qu'elles ne devraient pas.
5. Enregistrer la race
Un seul appel, dans un fichier shared :
CD.registerBreed({
key = "pug",
engineBreed = "pug",
typePrefix = "pug",
nameKey = "IGUI_PD_Breed_pug",
litter = { 1, 2 },
xpMult = { scent = 1.5, combat = 1, obedience = 2.0, hunt = 1.2, herding = 1.0 },
combatPower = 0.15,
lethalityCurve = { min = 0.40, max = 1.3 },
canKill = false,
canKnockdown = false,
combatStressMult = 0.9,
panicThreshold = 0.70,
bagMult = 0.6,
sentinelMult = 1.35,
barkNoiseMult = 1.5,
alertModeLocked = true,
loyaltyDecayMult = 0,
geneRange = {
strength = { 0.00, 0.10 },
aggressiveness = { 0.05, 0.20 },
resistance = { 0.00, 0.15 },
stress = { 0.55, 0.85 },
},
spawns = {
{ id = "pughouse", class = "house", suffix = "|pg", breed = "pug",
chance = function() return CD.strayChancePerHouse() / CD.PUG_HOUSE_RARITY end },
},
})
L'appel insère la race, enregistre ses trois types d'animal, reconstruit l'ordre de reproduction pour que votre animal entre dans les croisements, et enregistre les étapes de spawn.
Avec une définition invalide, l'appel renvoie nil au lieu de lever une erreur, donc le chargement continue. Chaque refus écrit une ligne nommée dans le log.
Champs obligatoires
| Champ | Ce que c'est |
|---|---|
key |
l'identifiant de votre race, unique parmi tous les mods. Ne le changez pas après la sortie. |
typePrefix |
préfixe des trois types d'animal : <prefix>pup, <prefix>female, <prefix>male. Choisit la mesh du corps. |
nameKey |
clé de traduction du nom affiché. |
xpMult |
vitesse d'apprentissage par compétence : scent, combat, obedience, hunt, herding. 1.0 est la normale. |
combatPower |
la force de ses coups. Le Caramelo est à 0.20, une race de combat passe au-dessus de 1.0. |
lethalityCurve |
{ min, max }, comment les dégâts montent de Combat 0 à Combat 10. |
geneRange |
fourchette de naissance des quatre gènes : strength, aggressiveness, resistance, stress. Chacune est un { low, high } entre 0 et 1. |
Champs optionnels
| Champ | Défaut | Ce qu'il fait |
|---|---|---|
engineBreed |
la key |
nom de race donné au moteur. Il choisit la texture. Unique par race. |
descKey |
IGUI_PD_BreedDesc_<key> |
clé de description. Plusieurs races peuvent partager une même clé. |
litter |
valeur de base | { min, max } chiots par naissance. |
canKill |
true | false veut dire qu'il épuise les zombies mais ne porte jamais le coup fatal. |
canKnockdown |
false | true lui permet de faire tomber un zombie. |
combatStressMult |
1 | ce qu'un combat lui coûte en stress. |
panicThreshold |
valeur de base | niveau de stress auquel il arrête de se battre. panicImmune = true veut dire qu'il ne s'arrête jamais. |
bagMult |
1 | multiplicateur de capacité des sacoches. |
puppySize |
0.6 | échelle visuelle absolue du chiot. Une petite race doit la régler, sinon le chiot naît à la taille d'un adulte. |
sentinelMult |
1 | multiplie le rayon final de la sentinelle. |
barkNoiseMult |
1 | multiplie le rayon et le volume de l'aboiement d'alarme. Avec l'option de bruit du bac à sable désactivée, aucune race n'attire les zombies en aboyant, quelle que soit la valeur. |
loyaltyDecayMult |
1 | multiplie la perte de loyauté quotidienne. 0 veut dire que le lien ne s'efface jamais. |
alertModeLocked |
false | l'animal reste en alerte complète : son maître ne peut le passer ni en discret ni en silencieux, nulle part. |
huntFetchLevel |
6 | niveau de Chasse à partir duquel il rapporte sa prise à son maître. |
huntDeliverTimeoutMin |
5 | minutes de jeu pendant lesquelles il continue d'essayer de livrer avant d'abandonner. |
distract |
désactivé | { <kind> = { chance = 0..1, ... } }. La race se lance d'elle-même sur ce qu'elle remarque, sans mode ni niveau requis. |
idleAnimMs |
valeur de base | durée de la fenêtre d'animation d'attente, en millisecondes. Réglez-la quand vos clips sont plus courts que ceux du chien, sinon la boucle repart et coupe le geste au milieu. |
restAnim |
true | false garde l'animal debout en Reste et en Garde au lieu de le coucher. |
restPoses |
couché | une liste de poses de repos dans laquelle l'animal tire à chaque fois qu'il s'installe, avec des transitions et des variations d'attente optionnelles. Demande vos propres clips et nœuds d'animation. |
limpAnim |
false | true fait boiter un animal blessé quand il marche. Ne le mettez que si votre modèle a les deux clips optionnels Rac_WalkLimpFront et Rac_WalkLimpBack (cycles de marche de 1.0 s avec le même root motion que Rac_Walk). Sans les clips, l'animal marche figé sur place. |
bandSkin |
désactivé | { base, front, back, cutFront, cutBack }, cinq noms de texture de corps. L'animal porte la variante coupée tant que la plaie saigne et la variante bandée une fois pansée, sur la patte blessée. Construisez les quatre variantes avec _dogrig/forge/_paw_band.py. Sans ça l'animal est blessé quand même, il ne montre simplement aucune marque. |
voices |
sons du chien | { bark, growl, idle, wildbark, pet, whine, eat, drink }. |
diet |
les listes du chien | ce que l'animal ne doit pas manger, ce qui compte comme viande pour lui, et ce qu'il mange tout seul dans une mangeoire. |
maleChance |
0.5 | part de la race qui naît mâle, de 0 à 1. |
sterileMale |
false | true tient les mâles de la race à l'écart de la reproduction. |
species |
"dog" |
la reproduction est fermée par espèce. Section 6. |
skills, canBreed, huntMaxPrey |
tout activé | les blocs structurels. Section 6. |
Les nombres et les drapeaux que vous ajoutez à la définition se relisent avec CD.breedNumber(animal, field) et CD.breedFlag(animal, field), pas avec CD.getBreedDef(animal).field. Ils gèrent un champ absent et continuent de marcher quand le mod de base change.
L'instinct : distract
distract = {
prey = { chance = 0.25 },
},
Une race qui le déclare va, de temps en temps, remarquer quelque chose et se lancer dessus, en lâchant ce qu'elle était en train de faire. Elle le fait depuis Reste comme depuis Suis moi. Tant que ça dure, les ordres reviennent refusés avec le message distracted que le jet d'obéissance utilise déjà. Viens ici est le seul ordre qui passe, et il annule la distraction.
Chaque type prend les mêmes réglages optionnels. Ce que vous laissez de côté prend les défauts du mod :
| Champ | Ce qu'il fait |
|---|---|
chance |
0..1, tiré seulement quand le déclencheur du type a trouvé quelque chose. Écrire 25 pour "25%" est rejeté avec une ligne nommée dans le log |
radius |
cases que le déclencheur fouille, quand le type fouille quelque chose |
durationMin |
minutes de jeu que dure la fenêtre avant qu'elle expire d'elle-même |
cooldownMin |
minutes de jeu avant que le même animal puisse être distrait de nouveau |
Le type prey
Le seul type que le mod de base livre. L'animal s'en prend aux animaux sauvages, et classes choisit ceux qui comptent :
distract = {
prey = { chance = 0.25, radius = 6, classes = { tiny = true, small = true } },
},
Deux limites s'appliquent quoi que vous déclariez :
- Il ne passe jamais au-dessus de votre
huntMaxPrey: une race plafonnée àsmallne poursuivra pas de cerf même si elle déclarelarge. - La barrière de niveau ne saute que pour
tiny. Un animal fraîchement apprivoisé avec Chasse 0 attrape déjà les rongeurs. Avecsmalldéclaré, la race poursuit le lapin à n'importe quel niveau, mais la mise à mort demande le même niveau de Chasse que pour le Labrador.
L'option de Chasse du bac à sable ne touche que la mise à mort : l'animal poursuit quand même, mais la proie s'échappe.
Une prise faite comme ça passe par la récupération normale, donc le huntFetchLevel de votre race décide s'il rapporte le corps à son maître. À 0 l'animal livre dès le premier jour, et c'est comme ça qu'un chat vous apporte un rat mort. La livraison survit à la fenêtre de distraction : une fois la proie morte, l'animal reprend les ordres, et le trajet du retour tourne sur son propre huntDeliverTimeoutMin.
Écrire votre propre type
distract est un répartiteur. Enregistrez le vôtre :
CD.registerDistraction("butterfly", {
gate = function(animal, cfg) return animal:isOutside() end, -- optional, cheap pre-check
find = function(animal, cfg) return findButterflyNear(animal, cfg.radius) end,
drive = function(animal, owner, d, cfg) return chaseIt(animal, d) end,
stop = function(animal, d) end, -- optional, cleanup
})
Déclarez-le ensuite sur la race comme n'importe quel autre type : distract = { butterfly = { chance = 0.10 } }.
gate tourne avant le budget global de balayage du mod. find tourne après le budget et fait la recherche coûteuse. drive renvoie true tant qu'il conduit encore l'animal et false quand il a fini, ce qui ferme la fenêtre. Votre handler tourne dans son propre pcall. S'il lève une erreur, le mod écrit une ligne nommée, désenregistre ce type pour la session et laisse le reste du compagnon en état de marche.
Noms réservés aux futurs types du mod de base : drink, eat, play. Vous pouvez enregistrer un type sous l'un de ces noms aujourd'hui, mais le mod de base reprend le nom le jour où il livre le sien.
Les poses de repos
Un animal au repos se couche. restPoses remplace cette pose unique par une liste dans laquelle l'animal tire à chaque fois qu'il s'installe :
restPoses = {
"cdRest", -- the base lie-down
{ var = "cdSit", enter = "cdSitIn", exit = "cdSitOut",
variation = { "cdSitGroom", "cdSitGroom2" } },
},
Une entrée est soit le nom d'une variable d'animation, soit une table. var est le booléen qui reste actif pendant toute la pause. enter et exit sont des impulsions uniques jouées quand l'animal s'installe et quand il se relève. variation est une liste d'impulsions uniques qu'il joue de temps en temps pendant qu'il tient la pose, sur la même horloge que la variation d'attente. Seul var est obligatoire.
Le tirage est uniforme et se fait à chaque entrée en repos, donc deux entrées valent 50% chacune et l'animal peut prendre deux fois la même de suite. Ce n'est pas une alternance.
Chaque impulsion dure ce que idleAnimMs dit pour cette variable, donc passez la forme en table et donnez une durée à chaque variable, y compris celles d'attente que vous aviez déjà. Donnez la vraie durée du clip.
idleAnimMs = { cdIdle2 = 3800, cdIdle3 = 3800, cdSitIn = 1375, cdSitOut = 1417,
cdSitGroom = 19833, cdSitGroom2 = 25167 },
Les clips et les nœuds sont les vôtres, pas ceux du mod de base. Chaque variable a besoin d'un fichier de nœud dans votre add-on sous media/AnimSets/raccoon/idle/, et le clip que son m_AnimName désigne doit exister dans votre modèle. Un nœud qui pointe sur un clip absent du modèle échoue en silence et l'animal reste dans l'animation d'attente de base. Donnez au nœud de pose un m_ConditionPriority de 10 comme ceux du mod de base, et un nombre plus grand à tout ce qui doit jouer par-dessus : 11 pour une variation, 12 pour les deux transitions. Un nœud joue son clip en boucle sauf si vous dites le contraire, donc chaque nœud censé ne jouer qu'une fois (les deux transitions et chaque variation) a besoin de <m_Looped>false</m_Looped>. Sans ça le clip repart à l'instant où il finit, l'animal revient visiblement en arrière et recommence, parce que la variable ne retombe qu'au tick serveur suivant. Gardez-les uniquement dans idle/. Un nœud de pose dans pathfind/ joue un clip sans root motion et fige l'animal là où il se trouve.
La voix
Sans voices, toute race utilise les sons du chien, donc un chat aboierait. Enregistrez d'abord vos sons :
CD.registerVoices({
CDCatMeow = "bark", CDCatGrowl = "bark", CDCatHiss = "bark", CDCatPurr = "bark",
CDCatPet = "fx", CDCatMeowAmbient = "ambient",
})
Pointez ensuite la race dessus :
voices = { bark = "CDCatMeow", growl = "CDCatGrowl", idle = "CDCatPurr",
wildbark = "CDCatMeowAmbient", pet = "CDCatPet", whine = "CDCatHiss" },
Sans CD.registerVoices les sons se jouent quand même, mais ils ignorent le curseur de catégorie du joueur et le volume des effets du jeu, et rien dans le log ne le signale. Une clé que vous laissez de côté dans voices retombe sur le son du chien.
whine est ce que dit l'animal quand il est blessé ou qu'il tombe malade. eat et drink sont les bruitages du repas. Ceux du chien sont des boucles que le mod de base arrête quand l'animal a fini, donc si les vôtres bouclent aussi, ajoutez-les à CD.SOUND_LOOPED et le mod de base les arrête également quand l'auditeur sort de portée. Un mod de base antérieur à l'API 11 ignore les trois clés et joue les sons du chien.
Le troisième argument est la portée audible de chaque son, en cases. Elle doit correspondre au distanceMax que vous avez écrit dans votre propre script de son :
CD.registerVoices(map, nil, { CDCatMeow = 22, CDCatGrowl = 10 })
Sur un serveur, le mod de base n'envoie un son qu'aux joueurs à l'intérieur de cette portée. Sans portée, la voix reçoit une portée générique par catégorie : un feulement qui porte à dix cases part vers tout le monde à trente. Passé distanceMax, l'atténuation inverse de FMOD cesse d'atténuer au lieu de se taire, donc un .ogg isolé continue de jouer à distanceMin / distanceMax de son volume à n'importe quelle distance. Gardez distanceMin petit sur vos voix fortes, pour la même raison.
Le régime
Sans diet, toute race mange comme un chien : la même nourriture qui l'empoisonne, la même qui compte comme viande, et la même qu'elle broute dans une mangeoire. Le champ remplace ces trois listes, pour votre race seulement.
diet = {
bad = { Fruits = true }, -- fruit now makes it sick
badParts = { onion = false }, -- onion no longer does
protein = { Cheese = true }, -- cheese now counts as meat
proteinParts = { tuna = true }, -- and so does anything with "tuna" in its type
trough = { Vegetables = false }, -- it will not graze vegetables
troughItems = { ["Base.CatFoodOpen"] = true },
},
Les deux formes existent parce que les catégories du jeu ne vont pas bien loin. Cheese est un vrai FoodType, donc une clé couvre tous les fromages du jeu et de tout mod de nourriture qui réutilise la catégorie. Le thon, non : la boîte ouverte est FoodType = Fish, avec tous les autres poissons, et la boîte fermée ne déclare aucun type de nourriture. badParts et proteinParts sont là pour atteindre ceux-là.
Chaque champ est une table de <key> = true (ajouter) ou <key> = false (retirer ce que le mod de base avait). Une valeur qui n'est ni true ni false est jetée avec une ligne nommée dans le log.
| Champ | Ce que sont ses clés |
|---|---|
bad |
FoodType d'une nourriture qui rend l'animal malade, lui coûte de la loyauté et passe en rouge dans le menu de repas |
badParts |
un morceau du type d'objet, en minuscules, cherché n'importe où dedans |
protein |
FoodType qui paie la dette de viande de l'animal, celle que le moodle Affaibli surveille |
proteinParts |
un morceau du type d'objet, même recherche que badParts |
nonProtein |
FoodType dont vous savez que ce n'est pas de la viande. Il ne fait que taire la ligne de log que le mod de base écrit pour un type de nourriture jamais vu |
trough |
FoodType et AnimalFeedType que l'animal mange tout seul dans une mangeoire |
troughItems |
type d'objet complet, pour un objet rangé dans la mangeoire qui n'a pas de type d'aliment à lui |
replace = true part d'une liste vide au lieu de celle du chien. Utilisez-le quand votre animal ne partage presque rien avec un chien ; un chat s'écrit d'habitude plus vite en une poignée d'ajouts et de retraits.
Les trois listes suivent l'animal, donc un chien et un chat devant la même mangeoire n'y mangent pas la même chose. La gamelle du mod est l'exception : elle stocke des points de nourriture, pas l'objet qui l'a remplie, donc la remplir passe par la liste de base pour tout le monde. De toute façon, rien de toxique n'entre dans une gamelle.
Le sexe à la naissance
Chaque animal est tiré mâle ou femelle quand il apparaît, moitié-moitié. maleChance est la part qui naît mâle, de 0 à 1, et sterileMale tient les mâles de la race à l'écart de la reproduction.
maleChance = 0.00033,
sterileMale = true,
Cette paire, c'est le Chat Tricolore. La mosaïque orange et noire est portée par le chromosome X, donc un chat tricolore est presque toujours une femelle, et le mâle qui sort quand même est XXY, environ un sur trois mille, et ne peut pas engendrer de portée. Un mâle d'une race sterileMale n'est jamais choisi comme partenaire, et l'option de reproduction disparaît de son menu contextuel. Les femelles se reproduisent normalement.
Un mâle d'une telle race est marqué "Stérile" partout où le jeu montre son sexe : le panneau de statut, le menu contextuel, l'inspection de l'animal errant et le chenil, pour que le joueur sache pourquoi il ne se reproduit pas.
Les deux champs sont lus dans la définition de race, donc rien n'est écrit sur l'animal et une sauvegarde existante n'a besoin d'aucune migration. Le même tirage décide du sexe d'un chiot à la naissance, donc une race qui est femelle dans le monde reste femelle dans les portées.
6. Désactiver tout un système
Les cinq compétences sont les cinq systèmes :
| Compétence | Le système correspondant |
|---|---|
scent |
la sentinelle : repérer les zombies et vous prévenir |
combat |
se battre, le mode Garde et l'auto-protection |
obedience |
l'arbre des tours |
hunt |
la chasse, plus le bonus de recherche qu'il donne à son maître |
herding |
s'occuper du bétail dans un enclos |
Une espèce déclare donc ce qu'elle n'a pas :
skills = { combat = false, herding = false },
canBreed = false,
huntMaxPrey = "tiny",
Absent ou true veut dire que l'animal a cette compétence.
Désactiver une compétence l'empêche aussi de gagner de l'expérience et retire la barre et ses ordres de l'interface.
canBreed = false est en dehors de la table parce que la reproduction n'a ni barre ni expérience. L'animal ne conçoit pas, n'est jamais choisi comme partenaire, et ne se reproduit pas non plus avec les siens.
L'espèce
Un add-on qui ajoute un autre animal, plutôt qu'un autre chien, la déclare :
CD.registerSpecies({ key = "cat",
nounKey = "IGUI_PD_SpeciesNoun_cat",
youngKey = "IGUI_PD_Young_cat" })
CD.registerBreed({ ..., species = "cat" })
Le champ est absent de toutes les races publiées jusqu'ici, et absent veut dire "dog".
La reproduction est fermée par espèce : un couple ne conçoit que si les deux côtés sont de la même espèce, dans la commande, dans le jet passif horaire et dans l'outil de debug. Et l'interface cesse d'appeler votre animal un chien : nounKey est le nom nu qui remplit des phrases comme "Ce %1 appartient à un autre survivant.", et youngKey est l'étiquette que porte le jeune de votre espèce dans la fenêtre, le menu contextuel et le chenil, là où un chien dit Chiot.
Ces deux noms ne couvrent que les phrases bâties autour d'un %1. Toute autre phrase où le mot chien est écrit en dur ("Inspecter le chien", "Votre chien a faim", "Le chien aboie exprès") prend une clé par espèce à partir de l'API 7 : livrez IGUI_PD_ScanStray_cat dans vos fichiers de traduction et le mod de base l'affiche à la place de IGUI_PD_ScanStray dès que l'animal à l'écran est un chat. Une clé que vous n'avez pas livrée retombe sur le texte du chien. Les clés qui acceptent le suffixe : ScanStray, ScanTitle, PauseGrowthTip, BadFood, AlertFullDesc, AlertQuietDesc, AlertLockedTip, HuntModeDesc, chaque Trick*Desc, TrickDistractDone, TrickNeedsBag, TrickGotoPick, TrickSniffNoItem, TrickFetchNoItem, AlertGroup, DogLost, chaque Refused_*, KennelStatusNoData et les Moodle_*_desc des moodles de soin (Sick, Weak, Hunger, Thirst, Rested, Grief). Les phrases sans animal à l'écran (l'onglet du registre, "Apprivoisez d'abord un animal.") sont déjà neutres en espèce dans le mod de base. Le chat les livre toutes en quatorze langues ; copiez son IG_UI.json comme modèle.
Le group de l'animal n'est lu que par le menu de spawn du debug et par le visionneur de clips, à travers IGUI_Animal_Group_<group>. Mettez t.group = "cat" après le helper de comportement et livrez cette clé, sinon votre espèce apparaît là-bas sous le nom du chien.
Déclarer species sans l'enregistrer n'est pas fatal : votre animal refuse quand même de se reproduire hors de son espèce, parce que la barrière compare la chaîne brute. Seuls les deux noms retombent sur ceux du chien, et le log le dit par son nom.
Sur un mod de base antérieur à l'API 6, le champ est ignoré sans ligne de log, et votre animal se reproduit comme un chien. S'il ne doit jamais se croiser avec un chien, gardez sur l'API 6, ou gardez canBreed = false tant que le mod de base est plus vieux, ce que fait le chat.
huntMaxPrey plafonne la taille des proies dans une chasse qui existe encore ; il ne remplace pas hunt = false. Il prend "tiny" (souris, rats, écureuils), "small" (lapins, ratons laveurs) ou "large" (cerfs, la valeur par défaut, sans plafond). Votre animal ne poursuit ni ne signale une proie au-dessus du plafond. Une valeur hors de cette échelle retombe sur "large" et écrit une ligne nommée dans le log.
Une option bloquée disparaît du radial, du menu contextuel et de la fenêtre, et le serveur la refuse sans message. Aucun de ces champs n'ajoute de clé de traduction, donc expliquez les limites dans la description de votre race.
7. Les définitions d'animal
Le moteur lie la mesh au type d'animal plutôt qu'à la race, donc votre race a besoin de trois types à elle. Le chemin le plus court est de copier le fichier d'un add-on existant et de le renommer.
Les trois blocs, dans un seul fichier shared sous Definitions/animal/ :
AnimalDefinitions.stages["pug"] = {}
AnimalDefinitions.stages["pug"].stages = {}
AnimalDefinitions.stages["pug"].stages["pugpup"] = {}
AnimalDefinitions.stages["pug"].stages["pugpup"].ageToGrow = 3 * 30
AnimalDefinitions.stages["pug"].stages["pugpup"].nextStage = "pugfemale"
AnimalDefinitions.stages["pug"].stages["pugpup"].nextStageMale = "pugmale"
AnimalDefinitions.breeds["pug"].breeds["pug"].texture = "Pug"
AnimalDefinitions.breeds["pug"].breeds["pug"].textureMale = "Pug"
AnimalDefinitions.breeds["pug"].breeds["pug"].rottenTexture = "Raccoon_Rotting"
AnimalDefinitions.breeds["pug"].breeds["pug"].invIconMale = "CDDogPaw_64"
AnimalDefinitions.breeds["pug"].breeds["pug"].invIconMaleDead = "CDDogPawDead_64"
local pugfemale = {}
CD.applyDogModel(pugfemale, "Pug_Body")
CD.applyDogBehaviour(pugfemale)
pugfemale.female = true
pugfemale.babyType = "pugpup"
pugfemale.minSize = 1.5
pugfemale.maxSize = 2.0
pugfemale.minWeight = 5
pugfemale.maxWeight = 8
pugfemale.wildFleeTimeUntilDeadTimer = CD.STRAY_NO_BLEEDOUT
pugfemale.breeds = AnimalDefinitions.breeds["pug"].breeds
pugfemale.stages = AnimalDefinitions.stages["pug"].stages
pugfemale.genes = AnimalDefinitions.genome["dog"].genes
AnimalDefinitions.animals["pugfemale"] = pugfemale
Les helpers que le mod de base vous donne :
| Helper | Ce qu'il remplit |
|---|---|
CD.applyCompanionModel(t, "Body_Model") |
modèle de corps, squelette, textures de découpe et animset |
CD.applyCompanionBehaviour(t) |
tous les drapeaux de comportement du moteur dont un compagnon a besoin |
CD.applyCompanionAvatar(t) |
la caméra du portrait pour la fenêtre de l'animal |
CD.COMPANION_SOUNDS |
la table commune des pas et des bruitages |
CD.defineGenome("species") |
le génome d'une nouvelle espèce, avec la liste de gènes standard (API 7) |
CD.STRAY_NO_BLEEDOUT |
la valeur qui empêche un errant effrayé de se vider de son sang |
CD.defineCompanionParts(typePrefix, engineBreed, meat) |
les morceaux de découpe des trois stades. meat est optionnel : { item, minNb, maxNb, pupMinNb, pupMaxNb }, et sans lui l'animal rend la viande de chien du mod de base. Le moteur multiplie le nombre et la faim de chaque morceau par la taille de la carcasse, donc une espèce de grande taille (le chat utilise 2.5 à 3.5 comme échelle visuelle) a besoin de son propre objet, avec une faim de base plus petite et un nombre bas |
Les noms neutres en espèce sont arrivés avec l'API 7. Les anciens, CD.applyDogModel, CD.applyDogBehaviour, CD.applyDogAvatar, CD.DOG_SOUNDS, CD.defineDogParts et CD.DogMoodles, sont les mêmes fonctions et les mêmes tables sous leur premier nom. Un add-on qui veut charger sur un mod de base antérieur à 0.7.3 garde sur les anciens noms et prend le nouveau quand il existe : local applyModel = CD.applyCompanionModel or CD.applyDogModel.
L'animset doit rester "raccoon". Un animset forké ne charge pas sa machine à états, et l'animal reste figé sur place.
Réutilisez le génome. Une race de chien pointe sur la liste du mod de base : AnimalDefinitions.genome["dog"].genes. Une nouvelle espèce appelle CD.defineGenome("cat") une fois et donne à chaque stade la table renvoyée. Le moteur ne lit que la liste genes de chaque stade, et la clé de génome est une convention, donc votre espèce a son entrée à elle sans liste de gènes qui lui soit propre.
Ne déclarez pas mate. Avec lui, le moteur lance son accouplement natif dans n'importe quelle zone à animaux, ce qui ignore le blocage de reproduction du mod et ses options de bac à sable, et donne des chiots sans lien. Le mod de base efface le champ au démarrage, mais ne comptez pas là-dessus.
Les morceaux de découpe sont obligatoires. Trois lignes dans un fichier à eux :
if CompanionDogs and CompanionDogs.defineDogParts then
CompanionDogs.defineDogParts("pug", "pug")
end
Sans eux, découper votre animal fait planter le code vanilla, parce qu'il indexe la définition des morceaux par type et par race et tombe sur un nil.
Le zoom du portrait dans CD.applyDogAvatar est calibré pour un chien, et le zoom d'avatar est un grossissement : plus l'animal est petit, plus le nombre est grand. Divisez le zoom par le rapport de taille face à un chien et multipliez les décalages par ce rapport, sinon votre animal se retrouve minuscule en bas de son propre portrait.
8. Le script du modèle
Un fichier sous media/scripts/ dit au jeu que votre modèle existe :
module Base
{
model Pug_Body
{
mesh = Skinned/Pug_Body,
shader = animalEffect,
static = false,
animationsMesh = PugAnim,
attachment head
{
offset = -0.0061 0.1671 0.0507,
rotate = -179.0 -5.0 95.0,
bone = Dummy01,
}
}
animationsMesh PugAnim
{
meshFile = Skinned/Pug_Body,
keepMeshAnimations = true,
}
}
keepMeshAnimations = true garde utilisables les clips qui sont dans votre fichier de modèle. Sans lui, le modèle charge et ne s'anime jamais.
L'attachement head est là où se posent les chapeaux et compagnie. Si vous voulez que les sacoches aillent à votre animal, ajoutez saddlebags_l, saddlebags_r et saddlebags_c de la même façon, sur l'os de la colonne. Trouvez ces nombres en regardant l'animal en jeu et en les poussant un peu. Des valeurs mesurées sur le modèle mettent les sacoches au centre géométrique de la mesh, qui n'est pas là où elles tombent bien sur le corps.
9. Où l'animal apparaît
Les errants sont tirés au sort par bâtiment, par morceau, la première fois que ce morceau se charge. Vous décrivez vos tirages comme une liste d'étapes, soit dans la définition de la race sous spawns, soit avec CD.registerStraySpawns(list).
spawns = {
{ id = "pughouse", class = "house", suffix = "|pg", breed = "pug", indoor = 85,
chance = function() return CD.strayChancePerHouse() / CD.PUG_HOUSE_RARITY end },
{ id = "pugpetvet", class = "petvet", suffix = "|pv", breed = "pug",
chance = function() return CD.PUG_PETVET_CHANCE * CD.dogSpawnMultiplier() end },
},
| Clé | Ce que c'est |
|---|---|
id |
nom de l'étape, pour vos propres logs |
class |
la classe de bâtiment sur laquelle elle tire |
chance |
une fonction qui renvoie un pourcentage, pour pouvoir lire le multiplicateur du bac à sable au moment du tirage. |
suffix |
clé du stockage persistant. Unique parmi tous les mods, et jamais changée. |
breed |
la clé de la race à faire naître |
gate |
fonction optionnelle qui renvoie un booléen, vérifiée avant le tirage. Le Husky utilise CD.isWinter. |
indoor |
pourcentage de chance de naître dans le bâtiment plutôt que dans la rue. Par défaut 35. |
Multipliez votre chance par CD.dogSpawnMultiplier(), ou divisez CD.strayChancePerHouse() par une rareté à vous. Dans les deux cas, l'option de bac à sable du joueur continue de fonctionner.
indoor est une préférence : un bâtiment sans case intérieure libre dans ce morceau retombe sur une case dehors.
Les classes de bâtiment
Le mod de base en enregistre quatre :
| Classe | Ce qui correspond |
|---|---|
house |
bâtiments résidentiels qui ne sont pas des commerces |
police |
postes de police et assimilés |
petvet |
animaleries et cliniques vétérinaires |
farm |
bâtiments de ferme, et elle est autorisée hors de la ville |
Si aucune ne convient, enregistrez la vôtre :
CD.registerBuildingClass("mineshaft", function(def) return def:isX() end,
{ skipUrbanGate = true })
La fonction de correspondance reçoit la définition du bâtiment et tourne dans un appel protégé, donc une erreur dedans n'arrête pas le chargement du morceau. skipUrbanGate laisse la classe tirer dans un morceau qui n'est pas urbain, ce dont une ferme ou une cabane de forêt a besoin. Ne définissez pas exclusive. Les classes du mod de base sont exclusives entre elles, et une classe d'add-on correspond par-dessus.
Les suffixes déjà pris
Le suffixe fait partie de la clé sous laquelle est enregistré "ce bâtiment a déjà été tiré". Deux mods qui utilisent le même suffixe corrompent les tirages l'un de l'autre.
Chaque suffixe commence par une barre verticale. Ceux-ci sont déjà pris :
- Base : le suffixe vide, et
g,h,hv,bc,gh,hh,bh - Rottweiler :
rw,r - Doberman :
db,dm,dh - Labrador :
lb,lk - Pug :
pg,pv - Malinois :
ml,mm,mh - Cats : tout suffixe qui commence par
ct,cvoucs(un jeu par pelage)
10. Le moodle de la race
Un moodle est un fichier client. Vous ajoutez une table à CD.DogMoodles, et le mod de base le dessine, le suit et le nettoie :
CD.DogMoodles[#CD.DogMoodles + 1] = {
id = "shadow",
breed = "pug",
nameKey = "IGUI_PD_Moodle_Shadow",
descKey = "IGUI_PD_Moodle_Shadow_desc",
icon = "CD_Moodle_Shadow",
fg = "CD_MoodShadowFG",
tintR = 0.92, tintG = 0.52, tintB = 0.68,
condition = function(player, dog)
if not dog or dog:isDead() then return 0 end
if CD.getBreed(dog) ~= "pug" then return 0 end
if CD.isDisloyal(dog) then return 0 end
return 1
end,
apply = function(player, dog, elapsedMin)
CD.relieveMood(player, CD.moodleReliefPerMin() * elapsedMin)
end,
}
condition renvoie le niveau, ou 0 pour "ne s'affiche pas". apply reçoit les minutes de jeu écoulées depuis le dernier appel, donc multipliez votre effet par elapsedMin.
Livrez l'icône en six tailles, de 32 à 128 pixels. L'interface choisit la taille selon la mise à l'échelle du joueur, et une taille manquante s'affiche comme un carré vide. L'icône apparaît dans la bande du mod lui-même, et non parmi les moodles habituels du jeu.
11. Hooks
Deux listes de fonctions que le mod de base appelle sur le serveur. Ajoutez-y depuis un fichier shared. Chaque handler tourne dans son propre appel protégé, donc une erreur dans un add-on n'arrête pas le mod de base.
CD.onHuntDelivered, depuis l'API 2, est appelée juste après que l'animal a déposé une prise aux pieds de son maître :
CD.onHuntDelivered[#CD.onHuntDelivered + 1] = function(animal, owner)
-- server side, so this is the right place to write mod data
end
CD.onUpkeepStress, depuis l'API 3, est appelée à chaque cycle d'upkeep, après que le mod de base a ajouté le stress des besoins non satisfaits et avant l'écriture. Renvoyez un nombre, qui est ajouté au total :
CD.onUpkeepStress[#CD.onUpkeepStress + 1] = function(animal, needsStress)
return CD.pugHeatRatio(animal) * CD.PUG_HEAT_STRESS_MAX
end
L'upkeep recalcule et écrase la valeur à chaque cycle, donc tout ce qui est écrit de l'extérieur se perd au tick suivant. Utilisez le hook pour une condition qui vient de l'environnement, comme la chaleur, le froid ou la pluie.
En multijoueur, le client n'écrit jamais de mod data. Une écriture côté client écrase la copie du serveur à la synchro suivante, et le symptôme apparaît des minutes plus tard, ailleurs, sans rapport. Faites le travail dans un fichier server, ou dans un de ces hooks.
Les nombres du panneau SERVER TUNING sont de simples constantes CD.*, et un add-on peut en assigner une depuis un fichier shared ou server, au chargement. À partir de l'API 9, le panneau prend la valeur en vigueur quand le serveur finit de charger comme valeur par défaut de ce réglage : elle tient jusqu'à ce qu'un admin tape par-dessus, le bouton de réinitialisation y revient, et le panneau l'affiche comme la valeur par défaut. Sur un mod de base plus ancien, le panneau réécrivait la valeur d'usine à chaque démarrage. Assignez au chargement, pas depuis un événement plus tardif, sinon le prochain changement sur n'importe quel réglage remet la valeur d'usine.
12. Les clés de traduction
Livrez un IG_UI.json par dossier de langue, sous media/lua/shared/Translate/<LANG>/. Le mod de base en livre quatorze : EN, PTBR, CH, CN, DE, ES, FR, IT, JP, KO, RU, TH, UA, VI.
Les clés dont une race a besoin :
| Clé | Où elle apparaît |
|---|---|
IGUI_PD_Breed_<key> |
le nom de la race, partout dans l'interface du mod |
IGUI_PD_BreedDesc_<key> |
la description sur la fiche de l'animal |
IGUI_Breed_<engineBreed> |
le nom de la race dans les écrans du jeu lui-même |
IGUI_AnimalType_<type> |
une pour chacun de vos trois types |
| les clés de votre espèce | le nounKey et le youngKey que vous avez passés à CD.registerSpecies |
| les clés de votre moodle | le nom et la description |
{
"IGUI_Breed_pug": "Pug",
"IGUI_AnimalType_pugmale": "Pug (male)",
"IGUI_AnimalType_pugfemale": "Pug (female)",
"IGUI_AnimalType_pugpup": "Pug (puppy)",
"IGUI_PD_Breed_pug": "Pug",
"IGUI_PD_BreedDesc_pug": "The house dog: the first companion whose worth is not fighting."
}
Un signe pourcent littéral dans une chaîne traduite fait planter l'écran qui l'affiche, parce que le formateur le lit comme un placeholder. Un fichier de langue avec un byte order mark ne compile pas, et l'erreur nomme un autre fichier que celui qui est cassé.
13. Ce que la sauvegarde retient
Trois de vos identifiants sont écrits dans les fichiers de sauvegarde et relus pour toujours :
key, l'identifiant de race stocké sur chacun de vos animauxtypePrefix, qui forme les types d'animal que le moteur stockesuffix, la clé du stockage "déjà tiré", une par étape de spawn
Aucun des trois ne peut être renommé après la sortie. Renommer une key laisse orphelin chaque animal vivant de cette race. Renommer un suffix fait tirer de nouveau chaque bâtiment de chaque sauvegarde existante, et un monde joué pendant des mois se remplit de votre animal.
Un nouveau suffixe est aussi la façon dont une race nouvelle atteint des mondes déjà en cours. CD: Cats a donné de nouveaux suffixes à ses quatre nouveaux pelages, donc ils ont commencé à apparaître dans les sauvegardes existantes, tandis que le chat noir d'origine a gardé son ancien suffixe et n'a pas été tiré de nouveau.
Retirer un add-on d'une sauvegarde qui a des animaux vivants de cette race les perd. Le moteur jette un animal dont il ne connaît plus le type. Le mod de base ne plante pas, et le lien et le chenil se dégradent proprement. Dites-le sur votre page Workshop.
Si votre add-on est absent, le mod de base retombe sur la race par défaut pour tout ce qu'on lui demande, donc une sauvegarde ouverte sans votre mod reste lisible.
14. Avant de publier
- La garde est dans chaque fichier Lua, et elle nomme l'API que vous utilisez vraiment.
- Le plancher de version du mod de base est écrit dans votre description, en toutes lettres.
versionMinne sait pas le dire. - Vous avez lu le log au premier chargement. Sinon
CD.registerBreedrefuse en silence, et chaque refus imprime une ligne nommée. CD.defineDogPartsest appelé. Dépecez un de vos animaux et vérifiez que ça ne plante pas.- Votre
key, votretypePrefixet vos suffixes de spawn sont uniques, et vous n'aurez pas à les changer. - Une petite race définit
puppySize, et vous avez vu un chiot pour le prouver. - Si c'est une autre espèce,
CD.registerVoicesest appelé etvoicesest rempli,whinecompris, sinon elle aboie et gémit comme un chien. - L'icône du moodle existe dans les six tailles.
- Chaque dossier de langue que vous livrez a toutes les clés. Une clé manquante affiche la clé brute à l'écran.
- Vous avez testé en multijoueur, et pas seulement en solo. La position, la propriété et les mod data s'y comportent tous différemment.
- Votre page Workshop met en garde contre le retrait de l'add-on d'une sauvegarde avec des animaux vivants.
15. Erreurs courantes
| Symptôme | Ce que c'est |
|---|---|
| "Mon animal n'apparaît jamais." | Presque toujours la garde : l'add-on a besoin d'une API que le mod de base installé n'a pas, donc il retourne tôt et n'enregistre rien. |
| "Il apparaît, mais il est figé." | L'animset n'est pas "raccoon", ou il manque des clips au modèle. |
| "Il s'affiche comme une tache noire et le log déborde." | Le modèle a plus de 60 os. |
| "Le dépecer fait planter le jeu." | CD.defineDogParts n'a pas été appelé. |
| "Le chiot naît à la taille d'un adulte." | Une petite race n'a pas défini puppySize. La valeur est absolue et est réappliquée à chaque passe, donc la plage de taille du type ne la couvre pas. |
| "Deux de mes races n'arrêtent pas de se transformer l'une en l'autre." | Elles partagent un engineBreed. Le préfixe choisit la mesh et peut être partagé par une famille. engineBreed choisit la texture et distingue les races de cette famille, donc il doit être unique par race. |
| "Le son se joue mais ignore le curseur de volume." | CD.registerVoices n'a pas été appelé pour ce son. |
| "Ça marche en solo et se comporte mal sur un serveur." | Quelque chose écrit des mod data sur le client. Déplacez-le sur le serveur. |
| "Il s'est battu avec un zombie alors que j'avais coupé le combat." | Le mod de base sur lequel il tourne est plus vieux que l'API 5. Ce mod de base ne connaît pas le champ et l'ignore. La garde doit bloquer sur ce mod de base au lieu de charger avec le champ absent. |
Quelque chose de faux dans ce manuel, ou qui manque ? Dites-le sur Discord.