Companion Dogs: Manual de Modding
Como fazer o seu addon: uma raça nova, ou uma espécie inteira, como mod separado da Workshop que se encaixa no Companion Dogs.
Este manual é para quem escreve mod. Se você só quer jogar, leia os outros dois manuais: Funcionalidades para o que o cão faz, e Raças para as raças.
Ele assume que você lê Lua e que tem um modelo com rig e animação para o seu bicho. O pipeline de arte não está aqui. A seção 2 lista o que o arquivo de modelo tem de cumprir.
Tudo que este manual nomeia é contrato público e existe para ser chamado de fora do mod. O que não está nomeado aqui é interno e pode mudar em qualquer versão, sem aviso.
1. O que é um addon
Um addon é um mod comum da Workshop que declara require=CompanionDogs e chama uma função no carregamento. Ele acrescenta uma raça (ou várias, se dividirem o mesmo corpo) e nada mais.
O base é dono de:
- a máquina de estados de animação, o animset e o esqueleto
- seguir, pathing, combate, caça, pastoreio, sentinela, necessidades e upkeep
- a interface inteira: a janela do cão, o radial, o menu de contexto, o canil, o marcador no mapa
- domesticação, vínculo, cruzamento, filhotes e mestiços
- as opções de sandbox e a replicação de multiplayer
O seu addon é dono de:
- os números que fazem o seu bicho ser diferente de um caramelo
- o modelo do corpo, a textura, o retrato e, se tiver, o ícone do moodle
- onde ele é encontrado no mundo
- o nome e a descrição, em cada idioma que você entregar
Duas cores que se comportam igual ainda são duas raças para o base (dois nomes, duas entradas no canil); você as escreve a partir de uma tabela compartilhada, como o CD: Cats faz com as cinco pelagens dele.
2. O que você precisa antes de começar
O seu bicho precisa do próprio .glb skinado em media/models_X/Skinned/, com o conjunto completo dos 21 clips de animação Rac_*. Clip não é herdado entre arquivos de modelo: um modelo com quinze clips dá um bicho que congela na primeira vez que alguém pedir um dos outros seis. O modelo também tem de ficar abaixo de 60 ossos e ter o nó de topo em identidade, senão a engine estoura uma vez por frame e o bicho aparece como um borrão preto. Mais dois clips são opcionais, Rac_WalkLimpFront e Rac_WalkLimpBack (ciclos de andar mancando); eles só importam se você marcar limpAnim = true na definição da raça.
Se você ainda não tem modelo, dá para escrever e testar todo o resto apontando o CD.applyDogModel para um dos corpos do base. O bicho parece um caramelo, mas todo sistema deste manual roda.
Você também precisa de uma textura para o corpo e de um retrato para a janela do canil.
Decida se o seu bicho é uma raça ou uma espécie antes de escrever qualquer código. Raça é um cão com números diferentes. Espécie é um bicho que não tem alguns dos trabalhos. A escolha muda quais campos você escreve; a seção 6 cobre isso.
3. Os arquivos de um addon
Este é o CD: Pug, o menor addon publicado, com as partes que importam:
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/
E o 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 é a linha importante. Ela garante que todo o Lua do base rodou antes do seu, em cada fase: shared primeiro, depois client, depois server. Sem ela os seus arquivos podem carregar primeiro, e toda chamada CD. vira índice em nil.
O versionMin não consegue dizer "precisa do Companion Dogs 0.6.8". Aquele campo só confere a versão do jogo, e não existe campo de mod.info para versão de dependência. O piso do base é garantido pela guarda da seção seguinte e anunciado ao jogador na sua descrição. Escreva lá, por extenso, ou o único sintoma que o jogador recebe é um bicho que nunca aparece.
4. A guarda de versão
Ponha isto no topo de todo arquivo Lua do seu addon, com o número de que o seu addon precisa:
local CD = CompanionDogs
if not (CD and CD.registerBreed and (CD.API_VERSION or 0) >= 3) then return end
Com o base ausente ou velho demais, o arquivo retorna na linha dois e o addon não faz nada: sem raça, sem spawn, sem moodle, sem linha no log.
A guarda é por arquivo, e cada arquivo guarda no que usa. O PugDefinitions.lua precisa dos helpers de modelo, então guarda neles:
if not (CompanionDogs and CompanionDogs.applyDogModel and CompanionDogs.DOG_SOUNDS) then return end
Não crie o global. CompanionDogs = CompanionDogs or {} num addon transforma um base ausente numa tabela meio construída, e toda guarda dali para baixo passa quando não deveria.
5. Registrando a raça
Uma chamada, num arquivo 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 },
},
})
Essa chamada insere a raça, registra os três tipos de animal dela, reconstrói a ordem de cruzamento para o seu bicho entrar na mestiçagem, e registra os passos de spawn.
Definição inválida faz a chamada devolver nil em vez de estourar, então o carregamento continua. Cada recusa imprime uma linha nomeada no log.
Campos obrigatórios
| Campo | O que é |
|---|---|
key |
o identificador da sua raça, único entre todos os mods. Não mude depois do release. |
typePrefix |
prefixo dos três tipos de animal: <prefix>pup, <prefix>female, <prefix>male. Escolhe a mesh do corpo. |
nameKey |
chave de tradução do nome de exibição. |
xpMult |
velocidade de aprendizado por habilidade: scent, combat, obedience, hunt, herding. 1.0 é o normal. |
combatPower |
o quanto ele bate. O Caramelo é 0.20, uma raça de briga passa de 1.0. |
lethalityCurve |
{ min, max }, como o dano cresce de Combate 0 até Combate 10. |
geneRange |
faixa de nascimento dos quatro genes: strength, aggressiveness, resistance, stress. Cada um é { low, high } dentro de 0 a 1. |
Campos opcionais
| Campo | Padrão | O que faz |
|---|---|---|
engineBreed |
o key |
nome de raça entregue à engine. Ele escolhe a textura. Único por raça. |
descKey |
IGUI_PD_BreedDesc_<key> |
chave da descrição. Várias raças podem dividir uma chave. |
litter |
valor do base | { min, max } de filhotes por parto. |
canKill |
true | false faz ele desgastar zumbi mas nunca dar o golpe final. |
canKnockdown |
false | true deixa ele derrubar zumbi. |
combatStressMult |
1 | o quanto uma briga custa de estresse para ele. |
panicThreshold |
valor do base | nível de estresse em que ele para de lutar. panicImmune = true significa que ele nunca para. |
bagMult |
1 | multiplicador da capacidade do alforje. |
puppySize |
0.6 | escala visual absoluta do filhote. Raça pequena tem de definir isto, senão o filhote nasce do tamanho de adulto. |
sentinelMult |
1 | multiplica o raio final de sentinela. |
barkNoiseMult |
1 | escala o raio e o volume do latido de alarme. Com a opção de ruído do sandbox desligada, nenhuma raça atrai zumbi com latido, seja qual for o valor. |
loyaltyDecayMult |
1 | multiplica o decaimento diário de lealdade. 0 significa que o vínculo nunca cai. |
alertModeLocked |
false | o bicho fica em alerta total: o dono não consegue pôr em Discreto nem em Silencioso por lugar nenhum. |
huntFetchLevel |
6 | nível de Caça a partir do qual ele traz o abate para o dono. |
huntDeliverTimeoutMin |
5 | minutos de jogo que ele insiste na entrega antes de desistir. |
distract |
desligado | { <kind> = { chance = 0..1, ... } }. A raça sai atrás de coisas sozinha, sem modo e sem nível. |
idleAnimMs |
valor do base | duração da janela da animação ociosa, em milissegundos. Ajuste quando os seus clips forem mais curtos que os do cão, senão o loop recomeça e corta o gesto no meio. |
restAnim |
true | false mantém o bicho em pé no Ficar e no Vigiar, em vez de deitado. |
restPoses |
deitar | lista de poses de descanso que o bicho sorteia toda vez que se acomoda, com transições e variações opcionais. Precisa de clips e nós de animação seus. |
limpAnim |
false | true faz o bicho ferido mancar ao andar. Só ligue quando o seu modelo tiver os dois clips opcionais Rac_WalkLimpFront e Rac_WalkLimpBack (ciclos de andar de 1.0 s com o mesmo root motion do Rac_Walk). Sem os clips o bicho anda congelado no lugar. |
bandSkin |
desligado | { base, front, back, cutFront, cutBack }, cinco nomes de textura do corpo. Na pata ferida, o bicho veste a variante cortada enquanto a ferida sangra e a enfaixada enquanto está com curativo. Monte as quatro variantes com _dogrig/forge/_paw_band.py. Sem ele o bicho se fere do mesmo jeito, só não mostra marca. |
voices |
sons de cão | { bark, growl, idle, wildbark, pet, whine, eat, drink }. |
diet |
as listas do cão | o que o bicho não pode comer, o que conta como carne para ele e o que ele come sozinho de um cocho. |
maleChance |
0.5 | fatia da raça que nasce macho, de 0 a 1. |
sterileMale |
false | true tira o macho da raça do cruzamento. |
species |
"dog" |
o cruzamento é fechado por espécie. Seção 6. |
skills, canBreed, huntMaxPrey |
tudo ligado | os bloqueios estruturais. Seção 6. |
Número e flag que você acrescentar à definição podem ser lidos de volta com CD.breedNumber(animal, field) e CD.breedFlag(animal, field), não com CD.getBreedDef(animal).field. Elas tratam o campo ausente e continuam funcionando quando o base mudar.
Instinto: distract
distract = {
prey = { chance = 0.25 },
},
Uma raça que declara isso, de tempos em tempos, repara em alguma coisa e vai atrás, largando o que estava fazendo. Ela faz isso tanto em Ficar quanto em Seguir. Enquanto dura, as ordens voltam recusadas com a mesma mensagem distracted que a rolagem de obediência já usa. O Vir aqui é o único comando que passa, e ele cancela a distração.
Todo tipo aceita os mesmos ajustes opcionais. O que você deixar de fora usa os padrões do mod:
| Campo | O que faz |
|---|---|
chance |
0..1, rolado só quando o gatilho daquele tipo achou alguma coisa. Escrever 25 por "25%" é recusado com uma linha nomeada no log |
radius |
tiles que o gatilho varre, quando aquele tipo varre alguma coisa |
durationMin |
minutos de jogo que a janela dura antes de expirar sozinha |
cooldownMin |
minutos de jogo até o mesmo bicho poder se distrair de novo |
O tipo prey
O único tipo que o mod base traz. O bicho vai atrás de animal selvagem, e classes escolhe quais contam:
distract = {
prey = { chance = 0.25, radius = 6, classes = { tiny = true, small = true } },
},
Dois limites valem independente do que você declarar:
- Nunca passa do seu
huntMaxPrey: uma raça com tetosmallnão persegue cervo nem declarandolarge. - O gate de nível só é pulado para
tiny. Um bicho recém-domesticado com Caça 0 já pega roedor. Comsmalldeclarado, a raça persegue o coelho em qualquer nível, mas o abate precisa do mesmo nível de Caça que o Labrador precisa.
O toggle de Caça do sandbox só afeta o abate: o bicho corre do mesmo jeito, mas a presa escapa.
O abate feito assim passa pela entrega normal, então o huntFetchLevel da sua raça decide se ela carrega o corpo até o dono. Em 0 o bicho entrega desde o primeiro dia, que é o que faz um gato te trazer um rato morto. A entrega sobrevive à janela de distração: morta a presa, o bicho volta a aceitar ordens, e a viagem de volta corre com o huntDeliverTimeoutMin dela.
Escrevendo o seu próprio tipo
distract é um dispatcher. Registre o seu:
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
})
Depois declare na raça como qualquer outro tipo: distract = { butterfly = { chance = 0.10 } }.
O gate roda antes do orçamento global de scan do mod. O find roda depois do orçamento e faz a busca cara. O drive devolve true enquanto ainda está conduzindo o bicho e false quando terminou, o que fecha a janela. O seu handler roda em pcall próprio. Se ele estourar, o mod escreve uma linha nomeada no log, desregistra aquele tipo pela sessão e deixa o resto do companheiro funcionando.
Nomes reservados para tipos futuros do base: drink, eat, play. Você pode registrar um tipo com um desses nomes hoje, mas o base toma o nome quando trouxer o dele.
Poses de descanso
Bicho descansando deita. O restPoses troca essa pose única por uma lista que ele sorteia toda vez que se acomoda:
restPoses = {
"cdRest", -- the base lie-down
{ var = "cdSit", enter = "cdSitIn", exit = "cdSitOut",
variation = { "cdSitGroom", "cdSitGroom2" } },
},
Cada entrada é o nome de uma variável de animação ou uma tabela. O var é o booleano que fica aceso a pausa inteira. O enter e o exit são pulsos de uma vez só, na hora de se acomodar e na hora de levantar. O variation é uma lista de pulsos que ele toca de vez em quando enquanto segura a pose, no mesmo relógio da variação ociosa. Só o var é obrigatório.
O sorteio é uniforme e acontece a cada entrada em descanso, então duas entradas dão 50% cada e o bicho pode tirar a mesma duas vezes seguidas. Não é alternância.
Cada pulso dura o que o idleAnimMs disser para aquela variável, então passe a forma de tabela e dê a duração de todas elas, inclusive as de ociosidade que você já usava. Ponha a duração real do clip.
idleAnimMs = { cdIdle2 = 3800, cdIdle3 = 3800, cdSitIn = 1375, cdSitOut = 1417,
cdSitGroom = 19833, cdSitGroom2 = 25167 },
Os clips e os nós são seus, não do base. Cada variável precisa de um arquivo de nó no seu addon, em media/AnimSets/raccoon/idle/, e o clip que o m_AnimName dele aponta tem que existir no seu modelo. Nó apontando para clip que o modelo não tem falha em silêncio e o bicho fica na ociosidade do base. Dê ao nó da pose um m_ConditionPriority de 10, como os do base, e um número maior para tudo que precisa tocar por cima dele: 11 para uma variação, 12 para as duas transições. Um nó toca o clip em loop a não ser que você diga o contrário, então todo nó feito para tocar uma vez só, tanto as transições quanto as variações, precisa de <m_Looped>false</m_Looped>. Sem isso o clip recomeça assim que termina e o bicho visivelmente volta atrás e refaz o movimento, porque a variável só apaga no tick seguinte do servidor. Mantenha todos só em idle/. Nó de pose em pathfind/ toca um clip sem root motion e congela o bicho onde ele está.
A voz
Sem voices toda raça usa os sons de cão, então um gato latiria. Registre os seus sons primeiro:
CD.registerVoices({
CDCatMeow = "bark", CDCatGrowl = "bark", CDCatHiss = "bark", CDCatPurr = "bark",
CDCatPet = "fx", CDCatMeowAmbient = "ambient",
})
Depois aponte a raça para eles:
voices = { bark = "CDCatMeow", growl = "CDCatGrowl", idle = "CDCatPurr",
wildbark = "CDCatMeowAmbient", pet = "CDCatPet", whine = "CDCatHiss" },
Sem o CD.registerVoices os sons até tocam, mas ignoram o slider de categoria do jogador e o volume de efeitos do jogo, e nada no log aponta para isso. Chave que você deixar de fora de voices cai no som de cão.
whine é o que o bicho diz quando se machuca ou adoece. eat e drink são o foley de comer e beber. Os de cão são loops que o base para quando o bicho termina; se os seus também forem loop, coloque-os em CD.SOUND_LOOPED e o base os para também quando o ouvinte sai do alcance. Base anterior à API 11 ignora as três chaves e toca os sons de cão.
O terceiro argumento é o alcance audível de cada som em tiles. Ele deve bater com o distanceMax que você escreveu no seu próprio script de som:
CD.registerVoices(map, nil, { CDCatMeow = 22, CDCatGrowl = 10 })
Num servidor o base manda o som só para os jogadores dentro desse alcance. Sem o alcance a voz cai num alcance genérico por categoria: um bufo que alcança dez tiles sai para todo mundo num raio de trinta. Passado o distanceMax o rolloff inverso do FMOD para de atenuar em vez de silenciar, então um .ogg solto segue tocando em distanceMin / distanceMax do volume a qualquer distância. Pelo mesmo motivo, mantenha o distanceMin baixo nas suas vozes altas.
A dieta
Sem diet toda raça come como cão: a mesma comida que o envenena, a mesma que conta como carne e a mesma que ele come de um cocho. O campo sobrescreve essas três listas só para a sua raça.
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 },
},
As duas formas existem porque as categorias do jogo só vão até certo ponto. Cheese é um FoodType de verdade, então uma chave cobre todo queijo do jogo e de qualquer mod de comida que reuse a categoria. Atum não: a lata aberta é FoodType = Fish, junto com todo peixe, e a lata fechada não declara tipo de comida nenhum. badParts e proteinParts são como você alcança esses casos.
Todo campo é uma tabela de <chave> = true (acrescenta) ou <chave> = false (tira o que o base tinha). Valor que não é true nem false é descartado com uma linha nomeada no log.
| Campo | O que são as chaves |
|---|---|
bad |
FoodType de comida que passa mal, custa lealdade e sai em vermelho no menu de alimentar |
badParts |
um pedaço do tipo do item, em minúsculo, procurado em qualquer posição |
protein |
FoodType que paga a dívida de carne do bicho, a que o moodle Fraco acompanha |
proteinParts |
um pedaço do tipo do item, mesma busca do badParts |
nonProtein |
FoodType que você sabe que não é carne. Só cala a linha de log que o base escreve para um tipo de comida que ele nunca viu |
trough |
FoodType e AnimalFeedType que o bicho come sozinho de um cocho |
troughItems |
tipo cheio do item, para item no cocho que não tem tipo de ração próprio |
replace = true começa de uma lista vazia em vez da do cão. Use quando o seu bicho não divide quase nada com um cachorro; um gato costuma sair mais fácil como um punhado de adições e remoções.
As três listas seguem o bicho, então um cão e um gato no mesmo cocho comem coisas diferentes dele. A tigela do mod é a exceção: ela guarda pontos de comida, não o item que a encheu, então encher segue a lista do base para todo mundo. Nada venenoso entra numa tigela de qualquer jeito.
Sexo ao nascer
Todo bicho é sorteado macho ou fêmea quando aparece, meio a meio. maleChance é a fatia que nasce macho, de 0 a 1, e sterileMale tira os machos da raça do cruzamento.
maleChance = 0.00033,
sterileMale = true,
Esse par é a gata tricolor. O mosaico de laranja e preto é carregado no cromossomo X, então a tricolor é quase sempre fêmea, e o macho que aparece é XXY, algo como 1 em 3000, e não gera ninhada. Macho de raça sterileMale nunca é escolhido como parceiro, e a opção de cruzar some do menu de contexto dele. As fêmeas cruzam normal.
Macho de uma raça dessas leva a marca "Estéril" em todo lugar onde o jogo mostra o sexo dele: a ficha, o menu de contexto, a varredura de vira-lata e o canil, para o jogador saber por que ele não cruza.
Os dois campos são lidos da definição da raça, então nada é escrito no bicho e save antigo não precisa de migração. O mesmo sorteio decide o sexo do filhote no parto, então raça que é fêmea no mundo continua fêmea nas ninhadas.
6. Desligando um sistema inteiro
As cinco habilidades são os cinco sistemas:
| Habilidade | O sistema que ela é |
|---|---|
scent |
o sentinela: perceber zumbi e avisar você |
combat |
brigar, o modo Vigiar e a auto-proteção |
obedience |
a árvore de truques |
hunt |
a caça, mais o bônus de forrageio que ele dá ao dono |
herding |
cuidar de gado num curral |
Então uma espécie declara o que não tem:
skills = { combat = false, herding = false },
canBreed = false,
huntMaxPrey = "tiny",
Ausente ou true significa que o bicho tem aquela habilidade.
Desligar uma habilidade também para o ganho de experiência e tira a barra e os comandos dela da interface.
canBreed = false fica fora da tabela porque cruzar não tem barra nem experiência. O bicho não concebe, nunca é escolhido como parceiro e não cruza nem com a própria raça.
A espécie
Um addon que acrescenta outro animal, em vez de outro cão, declara isto:
CD.registerSpecies({ key = "cat",
nounKey = "IGUI_PD_SpeciesNoun_cat",
youngKey = "IGUI_PD_Young_cat" })
CD.registerBreed({ ..., species = "cat" })
O campo está ausente em toda raça publicada até aqui, e ausente quer dizer "dog".
O cruzamento fica fechado por espécie: um par só concebe com os dois lados da mesma espécie, no comando, no sorteio passivo por hora e na ferramenta de debug. E a interface para de chamar o seu bicho de cão: nounKey é o substantivo cru que preenche frases como "Este %1 já pertence a outro sobrevivente", e youngKey é o rótulo que o filhote da sua espécie carrega na janela, no menu de contexto e no canil, onde um cão diz Filhote.
Esses dois substantivos só cobrem as frases montadas em volta de um %1. Toda outra frase que tem a palavra cão escrita no texto ("Inspecionar cão", "Seu cão está com fome", "O cão late de propósito") recebe uma chave por espécie a partir da API 7: entregue IGUI_PD_ScanStray_cat nos seus arquivos de tradução e o base mostra ela no lugar de IGUI_PD_ScanStray sempre que o bicho na tela é um gato. Chave que você não entregou cai no texto do cão. As chaves que aceitam o sufixo: ScanStray, ScanTitle, PauseGrowthTip, BadFood, AlertFullDesc, AlertQuietDesc, AlertLockedTip, HuntModeDesc, todo Trick*Desc, TrickDistractDone, TrickNeedsBag, TrickGotoPick, TrickSniffNoItem, TrickFetchNoItem, AlertGroup, DogLost, todo Refused_*, KennelStatusNoData e os Moodle_*_desc dos moodles de cuidado (Sick, Weak, Hunger, Thirst, Rested, Grief). Frase sem bicho na tela (a aba do registro, "Faça amizade com um animal antes") já é neutra de espécie no base. O gato entrega todas elas em catorze línguas; copie o IG_UI.json dele como gabarito.
O group do bicho só é lido pelo menu de spawn do debug e pelo visualizador de clips, pela chave IGUI_Animal_Group_<group>. Defina t.group = "cat" depois do helper de comportamento e entregue essa chave, ou sua espécie aparece lá com o nome do cão.
Declarar species sem registrar não é fatal: seu bicho continua recusando cruzar fora da própria espécie, porque o portão compara a string crua. Só os dois substantivos caem nos do cão, e o log diz isso pelo nome.
Num base anterior à API 6 o campo é ignorado sem linha de log, e seu bicho cruza como cão. Se ele nunca pode cruzar com um, guarde na API 6, ou mantenha canBreed = false enquanto o base for antigo, que é o que o gato faz.
huntMaxPrey é um teto de tamanho de presa dentro de uma caça que continua existindo; ele não substitui hunt = false. Aceita "tiny" (camundongo, rato, esquilo), "small" (coelho, guaxinim) ou "large" (cervo, o padrão, sem teto). Seu bicho não persegue nem aponta presa acima do teto. Valor fora dessa escada cai em "large" e imprime uma linha nomeada no log.
Opção bloqueada some do radial, do menu de contexto e da janela, e o server recusa sem mensagem. Nenhum desses campos traz chave de tradução, então explique os limites na descrição da sua raça.
7. As definições de animal
A engine amarra a mesh ao tipo de animal, e não à raça, então a sua raça precisa de três tipos próprios. O caminho mais curto é copiar o arquivo de um addon que já existe e renomear.
As três peças, num arquivo 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
Os helpers que o base te dá:
| Helper | O que ele preenche |
|---|---|
CD.applyCompanionModel(t, "Body_Model") |
modelo do corpo, esqueleto, texturas de abate e o animset |
CD.applyCompanionBehaviour(t) |
toda flag de comportamento da engine que um companheiro precisa |
CD.applyCompanionAvatar(t) |
a câmera do retrato na janela do bicho |
CD.COMPANION_SOUNDS |
a tabela compartilhada de passos e foley |
CD.defineGenome("species") |
o genoma de uma espécie nova, com a lista padrão de genes (API 7) |
CD.STRAY_NO_BLEEDOUT |
o valor que impede um vira-lata assustado de sangrar até morrer |
CD.defineCompanionParts(typePrefix, engineBreed, meat) |
as parts de abate dos três estágios. meat é opcional: { item, minNb, maxNb, pupMinNb, pupMaxNb }, e sem ele o animal rende a carne de cão do base. A engine multiplica a contagem e a fome de cada peça pelo tamanho da carcaça, então uma espécie com size alto (o gato usa 2,5 a 3,5 como escala visual) precisa de item próprio com fome base menor e contagem baixa |
Os nomes neutros de espécie chegaram com a API 7. Os antigos, CD.applyDogModel, CD.applyDogBehaviour, CD.applyDogAvatar, CD.DOG_SOUNDS, CD.defineDogParts e CD.DogMoodles, são as mesmas funções e tabelas com o primeiro nome delas. Addon que quer carregar num base anterior a 0.7.3 checa pelos nomes velhos e pega o novo quando ele existe: local applyModel = CD.applyCompanionModel or CD.applyDogModel.
O animset tem de continuar "raccoon". Um animset forkado não carrega a máquina de estados dele, e o bicho fica parado congelado no lugar.
Reaproveite o genoma. Raça de cão aponta para a lista do base: AnimalDefinitions.genome["dog"].genes. Espécie nova chama CD.defineGenome("cat") uma vez e dá a todo estágio a tabela que ele devolve. A engine só lê a lista genes de cada estágio, e a chave do genome é convenção, então sua espécie ganha entrada própria sem lista de genes própria.
Não declare mate. Com ele a engine roda o acasalamento nativo dela dentro de qualquer zona de animais, que ignora o bloqueio de cruzamento do mod e as opções de sandbox, e produz filhote sem vínculo. O base limpa o campo na inicialização, mas não conte com isso.
As parts são obrigatórias. Três linhas num arquivo só delas:
if CompanionDogs and CompanionDogs.defineDogParts then
CompanionDogs.defineDogParts("pug", "pug")
end
Sem elas, abater o seu bicho derruba o código vanilla, porque ele indexa a definição de parts por tipo e raça e cai num nil.
O zoom do retrato no CD.applyDogAvatar é calibrado para cão, e zoom de avatar é magnificação: quanto menor o bicho, maior o número. Divida o zoom pela razão de porte contra um cão e multiplique os offsets por ela, senão o seu bicho fica minúsculo no rodapé do próprio retrato.
8. O script do modelo
Um arquivo em media/scripts/ conta ao jogo que o seu 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 mantém utilizáveis os clips que estão dentro do seu arquivo de modelo. Sem isso o modelo carrega e nunca anima.
O attachment head é onde chapéu e afins ficam. Se você quer que o alforje sirva no seu bicho, acrescente saddlebags_l, saddlebags_r e saddlebags_c do mesmo jeito, no osso da coluna. Esses números se acham olhando o bicho em jogo e cutucando os valores. Valor medido do modelo põe a bolsa no centro geométrico da mesh, que não é onde ela fica bem no corpo.
9. Onde o bicho aparece
Vira-lata é sorteado por prédio, por chunk, na primeira vez que aquele chunk carrega. Você descreve os seus sorteios como uma lista de passos, ou dentro da definição da raça em spawns, ou com CD.registerStraySpawns(lista).
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 },
},
| Chave | O que é |
|---|---|
id |
nome do passo, para os seus próprios logs |
class |
a classe de prédio em que ele sorteia |
chance |
uma função que devolve uma porcentagem, para poder ler o multiplicador de sandbox na hora do sorteio. |
suffix |
chave do store persistido. Única entre todos os mods, e nunca muda. |
breed |
a key da raça a nascer |
gate |
função opcional que devolve booleano, conferida antes do sorteio. O husky usa CD.isWinter. |
indoor |
porcentagem de chance de nascer dentro do prédio em vez da rua. Padrão 35. |
Multiplique a sua chance por CD.dogSpawnMultiplier(), ou divida CD.strayChancePerHouse() por uma raridade sua. Dos dois jeitos a opção de sandbox do jogador continua valendo.
indoor é preferência: prédio sem tile interno livre naquele chunk cai de volta num tile lá fora.
As classes de prédio
O base registra quatro:
| Classe | O que casa |
|---|---|
house |
prédio residencial que não é loja |
police |
delegacia e afins |
petvet |
pet shop e clínica veterinária |
farm |
prédio de fazenda, e é permitido fora da cidade |
Se nenhuma servir, registre a sua:
CD.registerBuildingClass("mineshaft", function(def) return def:isX() end,
{ skipUrbanGate = true })
A função de match recebe a definição do prédio e roda dentro de uma chamada protegida, então um erro nela não derruba a carga do chunk. skipUrbanGate deixa a classe sortear em chunk que não é urbano, que é o que uma fazenda ou uma cabana de floresta precisa. Não defina exclusive. As classes do base são exclusivas entre si, e classe de addon casa por cima delas.
Sufixos já tomados
O sufixo é parte da chave sob a qual fica salvo "este prédio já foi sorteado". Dois mods usando o mesmo sufixo corrompem o sorteio um do outro.
Todo sufixo começa com uma barra vertical. Estes já estão em uso:
- Base: o sufixo vazio, e
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 sufixo que começa com
ct,cvoucs(um conjunto por pelagem)
10. O moodle da raça
Moodle é arquivo de client. Você acrescenta uma tabela em CD.DogMoodles, e o base desenha, acompanha e limpa:
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 devolve o nível, ou 0 para "não aparece". apply recebe os game-minutes desde a última chamada, então multiplique o seu efeito por elapsedMin.
Entregue o ícone em seis tamanhos, de 32 a 128 pixels. A interface escolhe o tamanho pela escala do jogador, e tamanho faltando aparece como um quadrado vazio. O ícone aparece na faixa do próprio mod, e não entre os moodles do vanilla.
11. Hooks
Duas listas de funções que o base chama no server. Acrescente nelas a partir de um arquivo shared. Cada handler roda dentro da própria chamada protegida, então um erro num addon não derruba o base.
CD.onHuntDelivered, da API 2, é chamada logo depois de o bicho largar o abate aos pés do dono:
CD.onHuntDelivered[#CD.onHuntDelivered + 1] = function(animal, owner)
-- server side, so this is the right place to write mod data
end
CD.onUpkeepStress, da API 3, é chamada a cada ciclo de upkeep, depois de o base somar o estresse das necessidades não atendidas e antes de gravar. Devolva um número, que é somado ao total:
CD.onUpkeepStress[#CD.onUpkeepStress + 1] = function(animal, needsStress)
return CD.pugHeatRatio(animal) * CD.PUG_HEAT_STRESS_MAX
end
O upkeep recomputa e sobrescreve o valor todo ciclo, então qualquer coisa escrita por fora se perde no tick seguinte. Use o hook para condição que vem do ambiente, como calor, frio ou chuva.
Em multiplayer, o client nunca escreve ModData. Escrita do lado do client sobrescreve a cópia do server no sync seguinte, e o sintoma aparece minutos depois, em outro lugar sem relação. Faça o trabalho num arquivo de server, ou num destes hooks.
Os números do painel SERVER TUNING são constantes CD.* comuns, e um addon pode atribuir uma delas num arquivo shared ou server, no load. A partir da API 9 o painel toma o valor em vigor quando o server termina de carregar como o padrão daquele controle: ele fica valendo até um admin digitar por cima, o botão de reset volta para ele, e o painel mostra esse número como padrão. Num base mais antigo o painel gravava o valor de fábrica de volta a cada boot. Atribua no load, não a partir de um evento posterior, senão a próxima mudança em qualquer controle devolve o valor de fábrica.
12. Chaves de tradução
Entregue um IG_UI.json por pasta de idioma, em media/lua/shared/Translate/<LANG>/. O base entrega quatorze: EN, PTBR, CH, CN, DE, ES, FR, IT, JP, KO, RU, TH, UA, VI.
As chaves de que uma raça precisa:
| Chave | Onde aparece |
|---|---|
IGUI_PD_Breed_<key> |
o nome da raça, em toda a interface do mod |
IGUI_PD_BreedDesc_<key> |
a descrição na ficha do bicho |
IGUI_Breed_<engineBreed> |
o nome da raça nas telas do próprio jogo |
IGUI_AnimalType_<type> |
uma para cada um dos seus três tipos |
| as chaves da sua espécie | o nounKey e o youngKey que você passou para CD.registerSpecies |
| as chaves do seu moodle | o nome e a descrição |
{
"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."
}
Um sinal de porcentagem literal numa string traduzida derruba a tela que a mostra, porque o formatador o lê como placeholder. Um arquivo de idioma com byte order mark não compila, e o erro nomeia um arquivo diferente do que está quebrado.
13. O que o save guarda
Três dos seus identificadores são escritos em arquivo de save e lidos de volta para sempre:
key, o identificador de raça guardado em cada bicho seutypePrefix, que forma os tipos de animal que a engine guardasuffix, a chave do store de "já foi sorteado", um por passo de spawn
Nenhum dos três pode ser renomeado depois do release. Renomear um key deixa órfão todo bicho vivo daquela raça. Renomear um suffix faz todo prédio de todo save existente sortear de novo, e um mundo jogado por meses enche do seu bicho.
Um sufixo novo também é o jeito de uma raça nova chegar a mundos que já estão rodando. O CD: Cats deu sufixos novos às quatro pelagens novas, então elas começaram a aparecer em saves existentes, enquanto o gato preto original manteve o sufixo antigo e não sorteou de novo.
Remover um addon de um save que tem bicho vivo daquela raça perde o bicho. A engine descarta um animal cujo tipo ela não conhece mais. O base não crasha, e o vínculo e o canil degradam sem erro. Avise na sua página da Workshop.
Se o seu addon estiver ausente, o base cai na raça padrão para qualquer pergunta que lhe façam, então um save aberto sem o seu mod é legível.
14. Antes de publicar
- A guarda está em todo arquivo Lua, e nomeia a API que você de fato usa.
- O piso de versão do base está escrito na sua descrição, por extenso. O
versionMinnão diz isso. - Você leu o log no primeiro carregamento. Fora isso o
CD.registerBreedrecusa em silêncio, e toda recusa imprime uma linha nomeada. - O
CD.defineDogPartsestá sendo chamado. Abata um bicho seu e veja que não crasha. - O seu
key, otypePrefixe os sufixos de spawn são únicos, e você não vai precisar mudar eles. - Raça pequena define
puppySize, e você viu um filhote para provar. - Se for outra espécie, o
CD.registerVoicesestá sendo chamado e ovoicesestá preenchido, comwhinedentro, senão ela late e geme como cão. - O ícone do moodle existe nos seis tamanhos.
- Toda pasta de idioma que você entrega tem todas as chaves. Chave faltando mostra a chave crua na tela.
- Você testou em multiplayer, e não só em single player. Posição, propriedade e ModData se comportam de forma diferente lá.
- A sua página da Workshop avisa para não remover o addon de um save com bicho vivo.
15. Erros comuns
| Sintoma | O que é |
|---|---|
| "O meu bicho nunca aparece." | Quase sempre a guarda: o addon precisa de uma API que o base instalado não tem, então ele retorna cedo e não registra nada. |
| "Ele aparece, mas fica congelado." | O animset não é "raccoon", ou faltam clips no modelo. |
| "Ele vira um borrão preto e o log enche." | O modelo tem mais de 60 ossos. |
| "Abater ele crasha." | O CD.defineDogParts não foi chamado. |
| "O filhote nasce do tamanho de um adulto." | Raça pequena que não definiu puppySize. O valor é absoluto e é reaplicado a cada sweep, então a faixa de tamanho do tipo não cobre isso. |
| "Duas raças minhas vivem virando uma na outra." | Elas dividem o engineBreed. O prefixo escolhe a mesh e pode ser dividido por uma família. O engineBreed escolhe a textura e separa as raças daquela família, então tem de ser único por raça. |
| "O som toca mas ignora o controle de volume." | O CD.registerVoices não foi chamado para aquele som. |
| "Funciona em single player e se comporta mal em servidor." | Alguma coisa está escrevendo ModData no client. Mova para o server. |
| "Ele brigou com zumbi mesmo eu tendo desligado o combate." | O base em que ele roda é mais velho que a API 5. Esse base não conhece o campo e ignora ele. A guarda tem de bloquear nesse base em vez de carregar com o campo faltando. |
Alguma coisa deste manual está errada, ou faltando? Fale no Discord.