Companion Dogs: Manual de modding
Cómo montar tu propio addon: una raza nueva, o una especie entera, como mod aparte de la Workshop que se engancha en Companion Dogs.
Este manual es para quien escribe mods. Si solo quieres jugar, lee los otros dos manuales: Funciones para lo que hace el perro, y Razas para las razas.
Da por hecho que sabes leer Lua y que tienes un modelo con rig y animación para tu animal. La tubería de arte no se cubre aquí. La sección 2 lista lo que tiene que cumplir el archivo de modelo.
Todo lo que se nombra en este manual es contrato público y está pensado para llamarse desde fuera del mod. Lo que no se nombra aquí es interno y puede cambiar en cualquier versión, sin aviso.
1. Qué es un addon
Un addon es un mod normal de la Workshop que declara require=CompanionDogs y llama a una función al cargar. Añade una raza (o varias, si comparten cuerpo) y nada más.
El base es dueño de:
- la máquina de estados de animación, el animset y el esqueleto
- seguir, el pathing, el combate, la caza, el pastoreo, el centinela, las necesidades y el mantenimiento
- toda la interfaz: la ventana del perro, el radial, el menú contextual, la perrera, la marca del mapa
- domesticar, el vínculo, la cría, los cachorros y los mestizos
- las opciones de sandbox y la replicación en multijugador
Tu addon es dueño de:
- los números que hacen que tu animal se distinga de un Caramelo
- su modelo de cuerpo, su textura, su retrato y, si lo tiene, su icono de moodle
- dónde se encuentra en el mundo
- su nombre y su descripción, en cada idioma que entregues
Dos colores que se comportan igual siguen siendo dos razas para el base (dos nombres, dos entradas en la perrera); los escribes desde una sola tabla compartida, como hace CD: Cats con sus cinco pelajes.
2. Lo que necesitas antes de empezar
Tu animal necesita su propio .glb con skin en media/models_X/Skinned/, con el juego completo de los 21 clips de animación Rac_*. Los clips no se heredan entre archivos de modelo: un modelo con quince clips te da un animal que se congela la primera vez que se le pide uno de los otros seis. El modelo también tiene que quedarse por debajo de 60 huesos y tener su nodo superior en identidad, o el motor lanza un error por fotograma y el animal se dibuja como un borrón negro. Hay dos clips más opcionales, Rac_WalkLimpFront y Rac_WalkLimpBack (ciclos de andar cojeando); solo importan si pones limpAnim = true en la definición de la raza.
Si todavía no tienes modelo, puedes escribir y probar todo lo demás apuntando CD.applyDogModel a uno de los cuerpos del base. El animal se ve como un Caramelo, pero funcionan todos los sistemas de este manual.
También necesitas una textura para el cuerpo y un retrato para la ventana de la perrera.
Decide si tu animal es una raza o una especie antes de escribir código. Una raza es un perro con números distintos. Una especie es un animal al que le faltan trabajos enteros. La elección cambia qué campos escribes; lo cubre la sección 6.
3. Los archivos de un addon
Esto es CD: Pug, el addon publicado más pequeño, con las partes que importan:
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/
Y el 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 es la línea importante. Garantiza que todo el Lua del base ha corrido antes que el tuyo, en cada fase: primero shared, luego client, luego server. Sin ella tus archivos pueden cargar antes y cada llamada CD. es un índice nil.
versionMin no sabe decir "necesita Companion Dogs 0.6.8". Ese campo solo controla la build del juego, y no hay ningún campo de mod.info para la versión de una dependencia. El mínimo del base lo impone la comprobación de la sección siguiente y se le anuncia al jugador en tu descripción. Escríbelo ahí, con palabras, o el único síntoma que le queda al jugador es un animal que nunca aparece.
4. La comprobación de versión
Pon esto arriba del todo en cada archivo Lua de tu addon, con el número que tu addon necesita:
local CD = CompanionDogs
if not (CD and CD.registerBreed and (CD.API_VERSION or 0) >= 3) then return end
Si el base falta o es demasiado antiguo, el archivo retorna en la línea dos y el addon no hace nada: ni raza, ni spawn, ni moodle, ni línea de log.
La comprobación es por archivo, y cada archivo comprueba lo que usa. PugDefinitions.lua necesita los helpers de modelo, así que comprueba esos:
if not (CompanionDogs and CompanionDogs.applyDogModel and CompanionDogs.DOG_SOUNDS) then return end
No crees el global. CompanionDogs = CompanionDogs or {} en un addon convierte un base ausente en una tabla a medio construir, y todas las comprobaciones que vienen después pasan cuando no deberían.
5. Registrar la raza
Una llamada, en un archivo 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 },
},
})
La llamada inserta la raza, registra sus tres tipos de animal, reconstruye el orden de cría para que tu animal entre en los cruces, y registra los pasos de spawn.
Una definición mala hace que la llamada devuelva nil en vez de lanzar un error, así que la carga sigue. Cada rechazo imprime una línea con nombre en el log.
Campos obligatorios
| Campo | Qué es |
|---|---|
key |
el identificador de tu raza, único entre todos los mods. No lo cambies después de publicar. |
typePrefix |
prefijo de los tres tipos de animal: <prefix>pup, <prefix>female, <prefix>male. Elige la malla del cuerpo. |
nameKey |
clave de traducción del nombre que se muestra. |
xpMult |
velocidad de aprendizaje por habilidad: scent, combat, obedience, hunt, herding. 1.0 es lo normal. |
combatPower |
cuánto pega. El Caramelo está en 0.20, una raza de pelea pasa de 1.0. |
lethalityCurve |
{ min, max }, cómo crece el daño de Combate 0 a Combate 10. |
geneRange |
rango de nacimiento de los cuatro genes: strength, aggressiveness, resistance, stress. Cada uno es { bajo, alto } dentro de 0 a 1. |
Campos opcionales
| Campo | Por defecto | Qué hace |
|---|---|---|
engineBreed |
la key |
nombre de raza que recibe el motor. Elige la textura. Único por raza. |
descKey |
IGUI_PD_BreedDesc_<key> |
clave de la descripción. Varias razas pueden compartir una clave. |
litter |
valor del base | { min, max } cachorros por parto. |
canKill |
true | false significa que desgasta a los zombis pero nunca da el golpe final. |
canKnockdown |
false | true le deja tirar al suelo a un zombi. |
combatStressMult |
1 | cuánto estrés le cuesta una pelea. |
panicThreshold |
valor del base | nivel de estrés al que deja de pelear. panicImmune = true significa que no para nunca. |
bagMult |
1 | multiplicador de la capacidad de las alforjas. |
puppySize |
0.6 | escala visual absoluta del cachorro. Una raza pequeña tiene que ponerlo, o el cachorro nace con tamaño de adulto. |
sentinelMult |
1 | multiplica el radio final del centinela. |
barkNoiseMult |
1 | escala el radio y el volumen del ladrido de alarma. Con la opción de ruido del sandbox apagada, ninguna raza atrae zombis con un ladrido, sea cual sea el valor. |
loyaltyDecayMult |
1 | multiplica la caída diaria de lealtad. 0 significa que el vínculo no se enfría nunca. |
alertModeLocked |
false | el animal se queda en alerta total: el dueño no puede ponerlo en discreto ni en silencio en ningún sitio. |
huntFetchLevel |
6 | nivel de Caza a partir del cual lleva la presa de vuelta a su dueño. |
huntDeliverTimeoutMin |
5 | minutos de juego que sigue intentando la entrega antes de rendirse. |
distract |
apagado | { <kind> = { chance = 0..1, ... } }. La raza va por su cuenta a por cosas, sin modo y sin nivel mínimo. |
idleAnimMs |
valor del base | duración de la ventana de animación de reposo, en milisegundos. Ponlo cuando tus clips sean más cortos que los del perro, o el bucle reinicia y corta el gesto a medias. |
restAnim |
true | false deja al animal de pie en Quieto y Guardia en vez de tumbarse. |
restPoses |
tumbarse | una lista de poses de descanso de la que el animal saca una cada vez que se acomoda, con transiciones y variaciones de reposo opcionales. Necesita clips y nodos de animación propios. |
limpAnim |
false | true hace que un animal herido cojee al andar. Ponlo solo si tu modelo tiene los dos clips opcionales Rac_WalkLimpFront y Rac_WalkLimpBack (ciclos de andar de 1.0 s con el mismo root motion que Rac_Walk). Sin los clips el animal anda congelado en el sitio. |
bandSkin |
apagado | { base, front, back, cutFront, cutBack }, cinco nombres de textura del cuerpo. En la pata herida, el animal lleva la variante con corte mientras la herida sangra y la vendada mientras tiene el vendaje puesto. Genera las cuatro variantes con _dogrig/forge/_paw_band.py. Sin esto el animal se hiere igual, solo que no muestra ninguna marca. |
voices |
sonidos de perro | { bark, growl, idle, wildbark, pet, whine, eat, drink }. |
diet |
las listas del perro | qué no puede comer el animal, qué cuenta como carne para él, y qué come por su cuenta de un comedero. |
maleChance |
0.5 | parte de la raza que nace macho, de 0 a 1. |
sterileMale |
false | true deja a los machos de la raza fuera de la cría. |
species |
"dog" |
la cría está cerrada por especie. Sección 6. |
skills, canBreed, huntMaxPrey |
todo encendido | los bloques estructurales. Sección 6. |
Los números y las banderas que añadas a la definición se leen luego con CD.breedNumber(animal, field) y CD.breedFlag(animal, field), no con CD.getBreedDef(animal).field. Estas dos aguantan un campo que falta y siguen funcionando cuando cambia el base.
Instinto: distract
distract = {
prey = { chance = 0.25 },
},
Una raza que lo declara se fija de vez en cuando en algo y va a por ello, dejando lo que estuviera haciendo. Lo hace tanto desde Quieto como desde Seguir. Mientras dura, las órdenes vuelven rechazadas con el mensaje distracted que ya usa la tirada de obediencia. Ven es la única orden que pasa, y cancela la distracción.
Todos los tipos aceptan los mismos ajustes opcionales. Lo que no pongas usa los valores del mod:
| Campo | Qué hace |
|---|---|
chance |
0..1, se tira solo cuando el disparador del tipo ha encontrado algo. Escribir 25 para decir "25%" se rechaza con una línea con nombre en el log |
radius |
casillas que busca el disparador, cuando el tipo busca algo |
durationMin |
minutos de juego que dura la ventana antes de caducar sola |
cooldownMin |
minutos de juego hasta que el mismo animal puede distraerse otra vez |
El tipo prey
El único tipo que trae el mod base. El animal va a por animales salvajes, y classes elige cuáles cuentan:
distract = {
prey = { chance = 0.25, radius = 6, classes = { tiny = true, small = true } },
},
Hay dos límites que se aplican declares lo que declares:
- Nunca pasa de tu
huntMaxPrey: una raza topesmallno persigue ciervos aunque declarelarge. - El nivel mínimo solo se salta con
tiny. Un animal recién domesticado con Caza 0 ya caza roedores. Consmalldeclarado, la raza persigue al conejo a cualquier nivel, pero para matarlo hace falta el mismo nivel de Caza que necesita el Labrador.
La opción de Caza del sandbox solo afecta a la muerte: el animal sigue persiguiendo, pero la presa se escapa.
Una presa muerta así pasa por la recogida normal, o sea que el huntFetchLevel de tu raza decide si lleva el cuerpo de vuelta al dueño. Con 0 el animal entrega desde el primer día, que es como un gato te trae una rata muerta. La entrega sobrevive a la ventana de distracción: en cuanto la presa está muerta el animal vuelve a aceptar órdenes, y el viaje de vuelta corre con su propio huntDeliverTimeoutMin.
Escribir tu propio tipo
distract es un despachador. Registra el tuyo:
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
})
Después decláralo en la raza como cualquier otro tipo: distract = { butterfly = { chance = 0.10 } }.
gate corre antes del presupuesto global de escaneo del mod. find corre después del presupuesto y hace la búsqueda cara. drive devuelve true mientras siga llevando al animal y false cuando termina, lo que cierra la ventana. Tu handler corre dentro de su propio pcall. Si lanza un error, el mod escribe una línea con nombre en el log, desregistra ese tipo durante la sesión y deja el resto del compañero funcionando.
Nombres reservados para tipos futuros del base: drink, eat, play. Puedes registrar hoy un tipo con uno de esos nombres, pero el base se queda con el nombre cuando publique el suyo.
Poses de descanso
Un animal descansando se tumba. restPoses cambia esa pose única por una lista de la que el animal saca una cada vez que se acomoda:
restPoses = {
"cdRest", -- the base lie-down
{ var = "cdSit", enter = "cdSitIn", exit = "cdSitOut",
variation = { "cdSitGroom", "cdSitGroom2" } },
},
Cada entrada es el nombre de una variable de animación o una tabla. var es el booleano que se queda encendido durante toda la pausa. enter y exit son pulsos de una sola vez que suenan cuando el animal se acomoda y cuando se levanta. variation es una lista de pulsos de una sola vez que toca de vez en cuando mientras mantiene la pose, con el mismo reloj que la variación de reposo. Solo var es obligatorio.
El sorteo es uniforme y ocurre en cada entrada en descanso, así que dos entradas son un 50% cada una y el animal puede sacar la misma dos veces seguidas. No es una alternancia.
Cada pulso dura lo que idleAnimMs diga para esa variable, así que pasa la forma de tabla y dale una duración a cada variable, incluidas las de reposo que ya tenías. Pon la duración real del clip.
idleAnimMs = { cdIdle2 = 3800, cdIdle3 = 3800, cdSitIn = 1375, cdSitOut = 1417,
cdSitGroom = 19833, cdSitGroom2 = 25167 },
Los clips y los nodos son tuyos, no del base. Cada variable necesita un archivo de nodo en tu addon bajo media/AnimSets/raccoon/idle/, y el clip al que apunta su m_AnimName tiene que existir en tu modelo. Un nodo que apunta a un clip que el modelo no tiene falla en silencio y el animal se queda de pie en el reposo del base. Dale al nodo de la pose una m_ConditionPriority de 10, como los del base, y un número mayor a todo lo que tenga que sonar por encima: 11 para una variación, 12 para las dos transiciones. Un nodo toca su clip en bucle salvo que digas lo contrario, así que todo nodo pensado para sonar una sola vez, tanto las transiciones como cada variación, necesita <m_Looped>false</m_Looped>. Sin eso el clip vuelve a empezar en cuanto termina y el animal da un salto atrás visible y lo repite, porque la variable solo se apaga en el siguiente tick del servidor. Déjalos todos solo en idle/. Un nodo de pose en pathfind/ toca un clip sin root motion y congela al animal donde está.
La voz
Sin voices todas las razas usan los sonidos del perro, así que un gato ladraría. Registra primero tus sonidos:
CD.registerVoices({
CDCatMeow = "bark", CDCatGrowl = "bark", CDCatHiss = "bark", CDCatPurr = "bark",
CDCatPet = "fx", CDCatMeowAmbient = "ambient",
})
Y luego apunta la raza a ellos:
voices = { bark = "CDCatMeow", growl = "CDCatGrowl", idle = "CDCatPurr",
wildbark = "CDCatMeowAmbient", pet = "CDCatPet", whine = "CDCatHiss" },
Sin CD.registerVoices los sonidos suenan igual, pero ignoran el deslizador de categoría del jugador y el volumen de efectos del juego, y nada en el log lo señala. Una clave que dejes fuera de voices cae en el sonido del perro.
whine es lo que el animal dice cuando se lastima o se enferma. eat y drink son el foley de comer y beber. Los del perro son bucles que la base detiene cuando el animal termina; si los tuyos también son bucles, ponlos en CD.SOUND_LOOPED y la base también los detiene cuando el oyente sale del alcance. Una base anterior a la API 11 ignora las tres claves y suena como perro.
El tercer argumento es el alcance audible de cada sonido en casillas. Tiene que coincidir con el distanceMax que escribiste en tu propio script de sonido:
CD.registerVoices(map, nil, { CDCatMeow = 22, CDCatGrowl = 10 })
En un servidor, el base manda un sonido solo a los jugadores dentro de ese alcance. Si dejas el alcance fuera, la voz recibe un alcance genérico por categoría: un bufido que llega a diez casillas sale para todos los que estén a treinta. Pasado el distanceMax, la caída inversa de FMOD deja de atenuar en vez de callarse, así que un .ogg suelto sigue sonando a distanceMin / distanceMax de su volumen a cualquier distancia. Por lo mismo, mantén distanceMin bajo en tus voces fuertes.
La dieta
Sin diet todas las razas comen como un perro: la misma comida que le sienta mal, la misma que cuenta como carne, y la misma que picotea de un comedero. El campo sustituye esas tres listas solo para tu raza.
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 },
},
Existen las dos formas porque las categorías del propio juego solo llegan hasta cierto punto. Cheese es un FoodType de verdad, así que una clave cubre todos los quesos del juego y de cualquier mod de comida que reutilice la categoría. El atún no: la lata abierta es FoodType = Fish, junto con todos los demás pescados, y la lata cerrada no declara ningún tipo de comida. badParts y proteinParts son la forma de llegar a esos.
Cada campo es una tabla de <key> = true (añadir) o <key> = false (quitar lo que traía el base). Un valor que no sea true ni false se descarta con una línea con nombre en el log.
| Campo | Qué son sus claves |
|---|---|
bad |
FoodType de la comida que sienta mal al animal, le cuesta lealtad y sale en rojo en el menú de dar de comer |
badParts |
un trozo del tipo de objeto, en minúsculas, buscado en cualquier parte del tipo |
protein |
FoodType que paga la deuda de carne del animal, la que vigila el moodle Débil |
proteinParts |
un trozo del tipo de objeto, con la misma búsqueda que badParts |
nonProtein |
FoodType que sabes que no es carne. Solo sirve para callar la línea de log que el base escribe ante un tipo de comida que no ha visto nunca |
trough |
FoodType y AnimalFeedType que el animal come por su cuenta de un comedero |
troughItems |
tipo de objeto completo, para un objeto del comedero que no tiene tipo de forraje propio |
replace = true empieza desde una lista vacía en vez de la del perro. Úsalo cuando tu animal no comparta casi nada con un perro; un gato suele salir más fácil como un puñado de altas y bajas.
Las tres listas van con el animal, así que un perro y un gato en el mismo comedero comen cosas distintas de él. El cuenco del propio mod es la excepción: guarda puntos de comida, no el objeto que lo llenó, así que llenarlo va por la lista del base para todos. De todos modos, en un cuenco no entra nada venenoso.
Sexo al nacer
Todo animal se sortea macho o hembra cuando aparece, mitad y mitad. maleChance es la parte que nace macho, de 0 a 1, y sterileMale deja a los machos de la raza fuera de la cría.
maleChance = 0.00033,
sterileMale = true,
Ese par es la gata calicó. El mosaico de naranja y negro va en el cromosoma X, así que una calicó es casi siempre hembra, y el macho que llega a salir es XXY, uno de cada tres mil, y no engendra camada. Un macho de una raza con sterileMale nunca es elegido como pareja, y la opción de criar desaparece de su menú contextual. Las hembras crían normal.
Un macho de una raza así lleva la marca "Estéril" en todos los sitios donde el juego muestra su sexo: la ficha, el menú contextual, el escaneo de callejeros y la perrera, para que el jugador sepa por qué no cría.
Los dos campos se leen de la definición de la raza, así que no se escribe nada en el animal y una partida existente no necesita migración. El mismo sorteo decide el sexo de un cachorro al nacer, así que una raza que es hembra en el mundo sigue siendo hembra en las camadas.
6. Apagar un sistema entero
Las cinco habilidades son los cinco sistemas:
| Habilidad | El sistema que es |
|---|---|
scent |
el centinela: detectar zombis y avisarte |
combat |
pelear, el modo Guardia y la defensa propia |
obedience |
el árbol de trucos |
hunt |
la caza, más el bonus de forrajeo que le da a su dueño |
herding |
cuidar del ganado en un corral |
Así que una especie declara lo que no tiene:
skills = { combat = false, herding = false },
canBreed = false,
huntMaxPrey = "tiny",
Ausente o true significa que el animal tiene esa habilidad.
Apagar una habilidad también corta su experiencia y quita de la interfaz su barra y sus órdenes.
canBreed = false va aparte de la tabla porque la cría no tiene ni barra ni experiencia. El animal no concibe, nunca sale elegido como pareja, y tampoco cría con los de su propia clase.
La especie
Un addon que añade otro animal, y no otro perro, lo declara:
CD.registerSpecies({ key = "cat",
nounKey = "IGUI_PD_SpeciesNoun_cat",
youngKey = "IGUI_PD_Young_cat" })
CD.registerBreed({ ..., species = "cat" })
El campo no está en ninguna raza publicada hasta ahora, y ausente significa "dog".
La cría está cerrada por especie: una pareja solo concibe si los dos lados son la misma especie, en la orden, en la tirada pasiva de cada hora y en la herramienta de depuración. Y la interfaz deja de llamar perro a tu animal: nounKey es el sustantivo pelado que rellena frases como "Este %1 ya pertenece a otro superviviente", y youngKey es la etiqueta que lleva la cría de tu especie en la ventana, el menú contextual y la perrera, donde un perro dice Cachorro.
Esos dos sustantivos solo cubren las frases construidas alrededor de un %1. Cualquier otra frase con la palabra perro escrita dentro ("Inspeccionar perro", "Tu perro tiene hambre", "El perro ladra a propósito") acepta una clave por especie a partir de la API 7: entrega IGUI_PD_ScanStray_cat en tus archivos de traducción y el base muestra esa en vez de IGUI_PD_ScanStray siempre que el animal en pantalla sea un gato. Una clave que no entregues cae en el texto del perro. Las claves que aceptan el sufijo: ScanStray, ScanTitle, PauseGrowthTip, BadFood, AlertFullDesc, AlertQuietDesc, AlertLockedTip, HuntModeDesc, todas las Trick*Desc, TrickDistractDone, TrickNeedsBag, TrickGotoPick, TrickSniffNoItem, TrickFetchNoItem, AlertGroup, DogLost, todas las Refused_*, KennelStatusNoData y las Moodle_*_desc de los moodles de cuidado (Enfermo, Débil, Hambre, Sed, Descanso, Duelo). Las frases sin animal en pantalla (la pestaña del registro, "Hazte amigo de un animal primero") ya son neutras en el base. El gato las entrega todas en catorce idiomas; copia su IG_UI.json como plantilla.
El group del animal solo lo leen el menú de spawn de depuración y el visor de clips, a través de IGUI_Animal_Group_<group>. Pon t.group = "cat" después del helper de comportamiento y entrega esa clave, o tu especie aparece ahí con el nombre del perro.
Declarar species sin registrarlo no es fatal: tu animal sigue negándose a criar fuera de su especie, porque la puerta compara la cadena tal cual. Solo los dos sustantivos caen en los del perro, y el log lo dice con nombre.
En un base anterior a la API 6 el campo se ignora sin línea de log, y tu animal cría como un perro. Si no puede cruzarse con uno nunca, comprueba la API 6, o deja canBreed = false mientras el base sea más antiguo, que es lo que hace el gato.
huntMaxPrey limita el tamaño de la presa dentro de una caza que sigue existiendo; no sustituye a hunt = false. Acepta "tiny" (ratones, ratas, ardillas), "small" (conejos, mapaches) o "large" (ciervos, el valor por defecto, sin límite). Tu animal no persigue ni señala presas por encima del límite. Un valor fuera de esa escalera cae en "large" e imprime una línea con nombre en el log.
Una opción bloqueada desaparece del radial, del menú contextual y de la ventana, y el servidor la rechaza sin mensaje. Ninguno de estos campos añade clave de traducción, así que explica los límites en la descripción de tu raza.
7. Las definiciones de animal
El motor ata la malla al tipo de animal y no a la raza, así que tu raza necesita tres tipos propios. El camino más corto es copiar el archivo de un addon que ya existe y renombrarlo.
Las tres piezas, en un solo archivo shared dentro de 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
Los helpers que te da el base:
| Helper | Qué rellena |
|---|---|
CD.applyCompanionModel(t, "Body_Model") |
modelo de cuerpo, esqueleto, texturas de despiece y el animset |
CD.applyCompanionBehaviour(t) |
todas las banderas de comportamiento del motor que necesita un compañero |
CD.applyCompanionAvatar(t) |
la cámara del retrato para la ventana del animal |
CD.COMPANION_SOUNDS |
la tabla compartida de pasos y foley |
CD.defineGenome("species") |
el genoma de una especie nueva, con la lista estándar de genes (API 7) |
CD.STRAY_NO_BLEEDOUT |
el valor que evita que un callejero asustado se desangre hasta morir |
CD.defineCompanionParts(typePrefix, engineBreed, meat) |
las piezas de despiece de las tres etapas. meat es opcional: { item, minNb, maxNb, pupMinNb, pupMaxNb }, y sin él el animal da la carne de perro del base. El motor multiplica tanto la cantidad como el hambre de cada pieza por el tamaño de la carcasa, así que una especie con un tamaño grande (el gato usa 2.5 a 3.5 como escala visual) necesita su propio objeto con menos hambre base y una cantidad baja |
Los nombres neutros llegaron con la API 7. Los antiguos, CD.applyDogModel, CD.applyDogBehaviour, CD.applyDogAvatar, CD.DOG_SOUNDS, CD.defineDogParts y CD.DogMoodles, son las mismas funciones y tablas con su primer nombre. Un addon que quiera cargar en un base anterior a 0.7.3 comprueba los nombres viejos y coge el nuevo cuando existe: local applyModel = CD.applyCompanionModel or CD.applyDogModel.
El animset tiene que seguir siendo "raccoon". Un animset bifurcado no carga su máquina de estados, y el animal se queda congelado en el sitio.
Reutiliza el genoma. Una raza de perro apunta a la lista del base: AnimalDefinitions.genome["dog"].genes. Una especie nueva llama a CD.defineGenome("cat") una vez y le da a cada etapa la tabla que devuelve. El motor solo lee la lista genes de cada etapa, y la clave del genoma es una convención, así que tu especie tiene su propia entrada sin lista de genes propia.
No declares mate. Con ese campo, el motor corre su apareamiento nativo dentro de cualquier zona de animales, que ignora el bloque de cría del mod y sus opciones de sandbox, y saca cachorros sin vínculo. El base limpia el campo al arrancar, pero no te apoyes en eso.
Las piezas son obligatorias. Tres líneas en su propio archivo:
if CompanionDogs and CompanionDogs.defineDogParts then
CompanionDogs.defineDogParts("pug", "pug")
end
Sin ellas, despiezar tu animal revienta el código vanilla, porque indexa la definición de piezas por tipo y raza y se encuentra un nil.
El zoom del retrato de CD.applyDogAvatar está calibrado para un perro, y el zoom del avatar es aumento: cuanto más pequeño el animal, mayor el número. Divide el zoom por la proporción de tamaño contra un perro y multiplica los offsets por ella, o tu animal sale diminuto al fondo de su propio retrato.
8. El script del modelo
Un archivo en media/scripts/ le dice al juego que tu modelo 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 mantiene utilizables los clips que van dentro de tu archivo de modelo. Sin eso el modelo carga y no se anima nunca.
El attachment head es donde se apoyan los sombreros y demás. Si quieres que las alforjas encajen en tu animal, añade saddlebags_l, saddlebags_r y saddlebags_c de la misma forma, sobre el hueso de la columna. Esos números se sacan mirando al animal en el juego y moviéndolos poco a poco. Los valores medidos sobre el modelo dejan la bolsa en el centro geométrico de la malla, que no es donde queda bien sobre el cuerpo.
9. Dónde aparece el animal
Los callejeros se tiran por edificio, por chunk, la primera vez que ese chunk carga. Tú describes tus tiradas como una lista de pasos, dentro de la definición de la raza como spawns, o con 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 },
},
| Clave | Qué es |
|---|---|
id |
nombre del paso, para tus propios logs |
class |
la clase de edificio sobre la que tira |
chance |
una función que devuelve un porcentaje, para poder leer el multiplicador del sandbox en el momento de la tirada. |
suffix |
clave del almacén persistente. Única entre todos los mods, y no se cambia nunca. |
breed |
la clave de raza que aparece |
gate |
función opcional que devuelve un booleano, se mira antes de la tirada. El Husky usa CD.isWinter. |
indoor |
probabilidad en porcentaje de nacer dentro del edificio en vez de en la calle. Por defecto 35. |
Multiplica tu probabilidad por CD.dogSpawnMultiplier(), o divide CD.strayChancePerHouse() por una rareza tuya. Las dos formas mantienen viva la opción de sandbox del jugador.
indoor es una preferencia: un edificio sin ninguna casilla interior libre en ese chunk cae en una casilla de fuera.
Las clases de edificio
El base registra cuatro:
| Clase | Qué encaja |
|---|---|
house |
edificios residenciales que no son tiendas |
police |
comisarías y similares |
petvet |
tiendas de mascotas y clínicas veterinarias |
farm |
edificios de granja, y esta sí vale fuera de la ciudad |
Si ninguna te sirve, registra la tuya:
CD.registerBuildingClass("mineshaft", function(def) return def:isX() end,
{ skipUrbanGate = true })
La función de encaje recibe la definición del edificio y corre dentro de una llamada protegida, así que un error suyo no corta la carga del chunk. skipUrbanGate deja que la clase tire en un chunk que no es urbano, que es lo que necesitan una granja o una cabaña en el bosque. No pongas exclusive. Las clases del base son exclusivas entre ellas, y una clase de addon encaja por encima.
Sufijos ya cogidos
El sufijo es parte de la clave con la que se guarda "este edificio ya se tiró". Dos mods con el mismo sufijo se corrompen las tiradas el uno al otro.
Todos los sufijos empiezan por una barra vertical. Estos ya están en uso:
- Base: el sufijo vacío, y
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: todo sufijo que empieza por
ct,cvocs(un conjunto por pelaje)
10. El moodle de la raza
Un moodle es un archivo de client. Añades una tabla a CD.DogMoodles, y el base lo dibuja, lo sigue y lo limpia:
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 devuelve el nivel, o 0 para "no se muestra". apply recibe los minutos de juego desde la última llamada, así que multiplica tu efecto por elapsedMin.
Entrega el icono en seis tamaños, de 32 a 128 píxeles. La interfaz elige el tamaño según el escalado del jugador, y un tamaño que falte sale como un cuadrado vacío. El icono aparece en la tira del propio mod, no entre los moodles vanilla.
11. Hooks
Dos listas de funciones que el base llama en el servidor. Añade a ellas desde un archivo shared. Cada handler corre dentro de su propia llamada protegida, así que un error de un addon no para el base.
CD.onHuntDelivered, desde la API 2, se llama justo después de que el animal suelte una presa a los pies de su dueño:
CD.onHuntDelivered[#CD.onHuntDelivered + 1] = function(animal, owner)
-- server side, so this is the right place to write mod data
end
CD.onUpkeepStress, desde la API 3, se llama en cada ciclo de mantenimiento, después de que el base haya sumado el estrés de las necesidades sin cubrir y antes de escribirlo. Devuelve un número, que se suma al total:
CD.onUpkeepStress[#CD.onUpkeepStress + 1] = function(animal, needsStress)
return CD.pugHeatRatio(animal) * CD.PUG_HEAT_STRESS_MAX
end
El mantenimiento recalcula y sobrescribe el valor en cada ciclo, así que lo que se escriba desde fuera se pierde en el tick siguiente. Usa el hook para una condición que venga del entorno, como calor, frío o lluvia.
En multijugador, el client nunca escribe mod data. Una escritura desde el client pisa la copia del servidor en la sincronización siguiente, y el síntoma sale minutos después en un sitio que no tiene nada que ver. Haz el trabajo en un archivo de server, o en uno de estos hooks.
Los números del panel SERVER TUNING son constantes CD.* normales, y un addon puede asignar una desde un archivo shared o de server al cargar. Desde la API 9 el panel toma el valor vigente cuando el servidor termina de cargar como el valor por defecto de ese control: se mantiene hasta que un admin escriba encima, el botón de reset vuelve a él, y el panel lo muestra como valor por defecto. En un base más viejo el panel volvía a escribir el valor de fábrica en cada arranque. Asigna al cargar, no desde un evento posterior, o el siguiente cambio en cualquier control devuelve el valor de fábrica.
12. Claves de traducción
Entrega un IG_UI.json por carpeta de idioma, en media/lua/shared/Translate/<LANG>/. El base entrega catorce: EN, PTBR, CH, CN, DE, ES, FR, IT, JP, KO, RU, TH, UA, VI.
Las claves que necesita una raza:
| Clave | Dónde sale |
|---|---|
IGUI_PD_Breed_<key> |
el nombre de la raza, en toda la interfaz del mod |
IGUI_PD_BreedDesc_<key> |
la descripción en la ficha del animal |
IGUI_Breed_<engineBreed> |
el nombre de la raza en las pantallas del propio juego |
IGUI_AnimalType_<type> |
una por cada uno de tus tres tipos |
| tus claves de especie | el nounKey y el youngKey que pasaste a CD.registerSpecies |
| tus claves de moodle | el nombre y la descripción |
{
"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 signo de porcentaje literal dentro de una cadena traducida revienta la pantalla que la muestra, porque el formateador lo lee como un marcador. Un archivo de idioma con marca de orden de bytes no compila, y el error nombra un archivo distinto del que está roto.
13. Lo que recuerda la partida
Tres de tus identificadores se escriben en los archivos de partida y se leen para siempre:
key, el identificador de raza guardado en cada animal tuyotypePrefix, que forma los tipos de animal que guarda el motorsuffix, la clave del almacén de "ya se tiró", uno por paso de spawn
Ninguno de los tres se puede renombrar después de publicar. Renombrar una key deja huérfano a cada animal vivo de esa raza. Renombrar un suffix hace que todos los edificios de todas las partidas existentes vuelvan a tirar, y un mundo jugado durante meses se llena de tu animal.
Un sufijo nuevo es también la forma de que una raza nueva llegue a mundos que ya están en marcha. CD: Cats les dio sufijos nuevos a sus cuatro pelajes nuevos, así que empezaron a salir en partidas existentes, mientras que el gato negro original se quedó con su sufijo viejo y no volvió a tirar.
Quitar un addon de una partida con animales vivos de esa raza los pierde. El motor descarta un animal cuyo tipo ya no conoce. El base no revienta, y el vínculo y la perrera se degradan sin romperse. Dilo en tu página de la Workshop.
Si tu addon falta, el base cae en la raza por defecto para todo lo que le pregunten, así que una partida abierta sin tu mod se puede leer.
14. Antes de publicar
- La comprobación está en cada archivo Lua, y nombra la API que de verdad usas.
- El mínimo de versión del base está escrito en tu descripción, con palabras.
versionMinno lo sabe decir. - Has leído el log en la primera carga. Si no,
CD.registerBreedrechaza en silencio, y cada rechazo imprime una línea con nombre. CD.defineDogPartsestá llamado. Despieza uno de tus animales y comprueba que no revienta.- Tu
key, tutypePrefixy tus sufijos de spawn son únicos, y no vas a necesitar cambiarlos. - Una raza pequeña pone
puppySize, y has visto un cachorro para comprobarlo. - Si es otra especie,
CD.registerVoicesestá llamado yvoicesestá relleno, conwhinedentro, o ladra y gime como un perro. - El icono del moodle existe en los seis tamaños.
- Cada carpeta de idioma que entregas tiene todas las claves. Una clave que falta muestra la clave cruda en pantalla.
- Lo has probado en multijugador, no solo en un jugador. La posición, la propiedad y el mod data se comportan de otra forma allí.
- Tu página de la Workshop avisa de lo que pasa al quitar el addon de una partida con animales vivos.
15. Errores comunes
| Síntoma | Qué es |
|---|---|
| "Mi animal no aparece nunca." | Casi siempre es la comprobación: el addon necesita una API que el base instalado no tiene, así que retorna pronto y no registra nada. |
| "Aparece, pero está congelado." | El animset no es "raccoon", o al modelo le faltan clips. |
| "Se dibuja como un borrón negro y el log se inunda." | El modelo tiene más de 60 huesos. |
| "Despiezarlo revienta." | No se llamó a CD.defineDogParts. |
| "El cachorro nace con tamaño de adulto." | Una raza pequeña no puso puppySize. El valor es absoluto y se vuelve a aplicar en cada pasada, así que el rango de tamaño del tipo no lo tapa. |
| "Dos razas mías se convierten la una en la otra." | Comparten engineBreed. El prefijo elige la malla y lo puede compartir una familia. engineBreed elige la textura y distingue entre las razas de esa familia, así que tiene que ser único por raza. |
| "El sonido suena pero ignora el deslizador de volumen." | No se llamó a CD.registerVoices para ese sonido. |
| "Funciona en un jugador y se porta mal en un servidor." | Algo escribe mod data en el client. Muévelo al server. |
| "Peleó con un zombi aunque apagué el combate." | El base sobre el que corre es anterior a la API 5. Ese base no conoce el campo y lo ignora. La comprobación tiene que bloquear en ese base en vez de cargar con el campo ausente. |
¿Algo de este manual está mal, o falta? Dilo en Discord.