Companion Dogs/模組製作
前往功能手冊前往品種手冊
加入 Discord
機器翻譯,尚未校閱。本頁是從葡萄牙文機器翻譯而來的,還沒有人校閱過,用詞和數字都可能有誤。有疑問時,請對照英文版或葡萄牙文原文。閱讀英文版

Companion Dogs:模組製作手冊

如何製作你自己的附加模組:一個新品種,或者一整個物種,作為獨立的創意工坊模組掛接到 Companion Dogs 上。

這本手冊是寫給做模組的人看的。如果你只是想玩,你要看的是另外兩本: 功能講狗狗能做什麼, 品種講品種本身。

本手冊假設你已經讀得懂 Lua,並且手上已經有一個附骨架與動畫的動物模型。美術流程不在這裡;第 2 節列出模型檔案必須符合的條件。

本手冊點名的一切都是公開契約,都是給模組外部呼叫用的。這裡沒有點名的東西屬於內部實作,可能在任何版本裡不經通知就改掉。

1. 附加模組是什麼

附加模組就是一個普通的創意工坊模組,它宣告 require=CompanionDogs,並在載入時呼叫一個函式。它加入一個品種(若共用同一副身體,也可以是好幾個),除此之外什麼都不加。

基礎模組擁有:

  • 動畫狀態機、動作集和骨架
  • 跟隨、尋路、戰鬥、狩獵、放牧、哨兵、需求和照料
  • 整套使用者介面:狗的視窗、輪盤、右鍵選單、犬舍、地圖標記
  • 馴服、建立羈絆、配種、幼犬和混血
  • 沙盒選項,以及多人遊戲的同步

你的附加模組擁有:

  • 讓你的動物和小卡犬不一樣的那些數字
  • 牠的身體模型、貼圖、肖像,如果有的話還有 moodle 圖示
  • 牠在世界上出現的地點
  • 牠的名字和描述,你附上的每一種語言都要

兩種行為完全相同的毛色,對基礎版來說仍然是兩個品種(兩個名字,犬舍裡兩個條目);你從同一張共用的表把它們寫出來,就像 CD: Cats 對它的五種毛色做的那樣。

2. 動手之前要準備什麼

你的動物需要自己的 skinned .glb,放在 media/models_X/Skinned/ 底下,並且要有完整的 21 個 Rac_* 動畫片段。片段不會在模型檔案之間繼承:只有十五個片段的模型,會在第一次被要求做另外六個之一的時候當場凍住。模型還必須維持在 60 根骨頭以內,而且頂層節點要在單位變換上,否則引擎每一幀都拋一次例外,動物會被畫成一團黑影。另外還有兩個選用片段, Rac_WalkLimpFront 和 Rac_WalkLimpBack(跛行的走路循環);只有你在品種定義裡設了 limpAnim = true 它們才有意義。

如果你還沒有模型,可以把 CD.applyDogModel 指向基礎版的某一具身體,其他所有東西照樣寫得出來也測得了。動物看起來像小卡犬,但這本手冊裡的每一套系統都在跑。

你還需要身體的貼圖,以及犬舍視窗用的肖像。

動手寫程式之前先決定:你的動物是一個品種還是一個物種。品種是數字不一樣的狗。物種是完全缺少某些工作的動物。這個選擇改變你要寫哪些欄位;第 6 節講這件事。

3. 一個附加模組的檔案

這是 CD: Pug,已發佈的附加模組裡最小的一個,只列出有意義的部分:

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/

還有 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 是最重要的那一行。它保證基礎版所有的 Lua 都在你的任何一行之前跑完,每一個階段都是:先 shared,再 client,再 server。沒有它,你的檔案可能先載入,於是每一個 CD. 呼叫都是對 nil 取索引。

versionMin 表達不了「需要 Companion Dogs 0.6.8」。那個欄位只管遊戲版本,而 mod.info 裡沒有表達相依版本的欄位。基礎版的下限靠下一節的守衛執行,並且靠你的描述告訴玩家。用文字寫在那裡,否則玩家得到的唯一症狀就是一隻永遠不出現的動物。

4. 版本守衛

把這段放在你附加模組的每一個 Lua 檔案最上面,數字換成你的附加模組需要的那個:

local CD = CompanionDogs
if not (CD and CD.registerBreed and (CD.API_VERSION or 0) >= 3) then return end

基礎版不存在或者太舊時,檔案在第二行就 return,附加模組什麼都不做:沒有品種、沒有生成、沒有 moodle、也沒有日誌。

守衛是逐檔案的,每個檔案守衛它自己用到的東西。PugDefinitions.lua 需要模型輔助函式,所以它守衛那些:

if not (CompanionDogs and CompanionDogs.applyDogModel and CompanionDogs.DOG_SOUNDS) then return end

不要去建立那個全域變數。在附加模組裡寫 CompanionDogs = CompanionDogs or {} 會把「基礎版不存在」變成一張半成品的表,於是下游每一道守衛都會在不該通過的時候通過。

5. 註冊品種

一次呼叫,寫在一個 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 },
    },
})

這次呼叫會插入品種、註冊牠的三個動物類型、重建配種順序讓你的動物加入混血,並註冊生成步驟。

定義寫壞的話,這次呼叫回傳 nil 而不是拋例外,載入照樣繼續。每一次拒絕都會在日誌裡印出一行帶名字的記錄。

必填欄位

欄位 是什麼
key 你的品種識別碼,在所有模組之間唯一。發佈之後不要改。
typePrefix 三個動物類型的前綴:<prefix>pup、<prefix>female、<prefix>male。決定用哪一副身體 mesh。
nameKey 顯示名稱的翻譯鍵。
xpMult 各技能的學習速度:scent、combat、obedience、hunt、herding。1.0 是正常。
combatPower 牠打得多痛。小卡犬是 0.20,戰鬥品種在 1.0 以上。
lethalityCurve { min, max },傷害從戰鬥 0 級長到 10 級的方式。
geneRange 四個基因的出生區間:strength、aggressiveness、resistance、stress。每一個都是 0 到 1 之間的 { low, high }。

選填欄位

欄位 預設 有什麼用
engineBreed 用 key 給引擎的品種名。它決定貼圖。每個品種唯一。
descKey IGUI_PD_BreedDesc_<key> 描述用的鍵。好幾個品種可以共用一個鍵。
litter 基礎值 { min, max },每胎的幼犬數。
canKill true false 表示牠能把殭屍磨到剩一口氣,但永遠補不上最後一下。
canKnockdown false true 讓牠能撞倒殭屍。
combatStressMult 1 一場架讓牠付出多少壓力。
panicThreshold 基礎值 牠停手不打的壓力值。panicImmune = true 表示牠永遠不停。
bagMult 1 馱袋容量的倍率。
puppySize 0.6 幼犬的絕對視覺縮放。小型品種一定要設,不然幼犬生下來就是成年大小。
sentinelMult 1 乘上最終的哨兵半徑。
barkNoiseMult 1 縮放警戒吠叫的半徑和音量。沙盒的噪音選項關掉時,不管值是多少,沒有任何品種的吠叫招得來殭屍。
loyaltyDecayMult 1 乘上每日的忠誠度衰減。0 表示羈絆永遠不淡。
alertModeLocked false 動物固定在全開的警報上:主人在任何地方都設不了靜默或靜音。
huntFetchLevel 6 從哪一級狩獵開始,牠會把獵物叼回給主人。
huntDeliverTimeoutMin 5 放棄之前牠持續嘗試送達的遊戲分鐘數。
distract 關 { <kind> = { chance = 0..1, ... } }。品種會自己去追某些東西,不需要模式也不需要等級。
idleAnimMs 基礎值 待機動畫視窗的長度,以毫秒計。當你的片段比狗的短時要設,否則循環會重來,把動作從中間切斷。
restAnim true false 讓動物在停留和守衛時站著,而不是趴下。
restPoses 趴下 一份休息姿勢清單,動物每次安頓下來都從裡面抽一個,可選帶進出過渡和待機變化。需要你自己的片段和動畫節點。
limpAnim false true 讓受傷的動物走路時跛行。只有你的模型有那兩個選用片段 Rac_WalkLimpFront 和 Rac_WalkLimpBack(1.0 秒的走路循環,root motion 和 Rac_Walk 相同)時才設。沒有那兩個片段,動物會原地凍住地走。
bandSkin 關 { base, front, back, cutFront, cutBack },五個身體貼圖名。受傷的腿在傷口流血時換成帶傷口的版本,包紮後換成纏繃帶的版本。四個變體用 _dogrig/forge/_paw_band.py 產生。不設的話動物照樣會受傷,只是看不到痕跡。
voices 狗的聲音 { bark, growl, idle, wildbark, pet, whine, eat, drink }。
diet 狗的那幾張清單 這隻動物不能吃什麼、對牠來說什麼算肉、牠會自己從飼料槽吃什麼。
maleChance 0.5 該品種生為公的比例,0 到 1。
sterileMale false true 讓該品種的公不參與配種。
species "dog" 配種按物種封閉。第 6 節。
skills、canBreed、huntMaxPrey 全開 結構性的區塊。第 6 節。

你加進定義裡的數字和旗標,要用 CD.breedNumber(animal, field) 和 CD.breedFlag(animal, field) 讀回來,不要用 CD.getBreedDef(animal).field。前兩個會處理欄位不存在的情況,而且基礎版改動之後照樣能用。

本能:distract

distract = {
    prey = { chance = 0.25 },
},

宣告了這個的品種,會時不時注意到某個東西然後追過去,把手上正在做的事丟下。牠在停留時會這麼做,在跟隨時也會。持續期間,指令會被回絕,用的是服從判定本來就在用的那句 distracted 訊息。過來是唯一穿得過去的指令,而且它會取消這次分心。

每一種都吃同一組選用旋鈕。你沒寫的部分用模組的預設值:

欄位 有什麼用
chance 0..1,只有在這一種的觸發真的找到東西時才擲。寫 25 當成「25%」會被拒絕,並在日誌裡留下一行帶名字的記錄
radius 觸發搜尋的格數,前提是這一種真的會搜尋
durationMin 這個視窗自己過期之前持續的遊戲分鐘數
cooldownMin 同一隻動物再次分心之前要等的遊戲分鐘數

prey 種類

基礎模組唯一附帶的一種。動物會去追野生動物,而 classes 決定哪些算數:

distract = {
    prey = { chance = 0.25, radius = 6, classes = { tiny = true, small = true } },
},

不管你宣告什麼,有兩條限制都成立:

  • 牠永遠不會超過你的 huntMaxPrey:上限卡在 small 的品種,就算宣告 large 也不會去追鹿。
  • 只有 tiny 會跳過等級關卡。剛馴服、狩獵 0 級的動物就已經抓得到鼠類。宣告了 small 的話,這個品種在任何等級都會去追兔子,但要殺掉牠需要和拉布拉多一樣的狩獵等級。

沙盒的狩獵開關只影響殺這一步:動物照樣去追,但獵物會跑掉。

用這種方式殺到的獵物走的是正常的回收流程,所以你品種的 huntFetchLevel 決定牠會不會把屍體叼回給主人。設成 0 的話動物從第一天就會送過來,貓叼一隻死老鼠給你就是這樣來的。送達這件事的壽命比分心視窗長:獵物一死,動物就重新聽指令,而回家那一趟跑的是它自己的 huntDeliverTimeoutMin。

撰寫你自己的種類

distract 是一個分派器。註冊你自己的:

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
})

然後像其他任何一種那樣宣告在品種上:distract = { butterfly = { chance = 0.10 } }。

gate 在模組的全域掃描預算之前跑。find 在預算之後跑,做那個花錢的搜尋。 drive 在還在驅動這隻動物的期間回傳 true,做完了回傳 false,這會關掉視窗。你的處理函式跑在自己的 pcall 裡。如果它拋例外,模組會記一行帶名字的日誌,把那一種在這次工作階段裡取消註冊,其餘的夥伴功能照常。

保留給未來基礎版種類的名字:drink、eat、play。你今天可以用其中一個名字註冊一種,但基礎版推出自己的那一版時會把名字接管過去。

休息姿勢

休息的動物會趴下。restPoses 把這唯一的姿勢換成一份清單,動物每次安頓下來都從裡面抽一個:

restPoses = {
    "cdRest",                                          -- the base lie-down
    { var = "cdSit", enter = "cdSitIn", exit = "cdSitOut",
      variation = { "cdSitGroom", "cdSitGroom2" } },
},

每一項要麼是一個動畫變數的名字,要麼是一張表。var 是整個休息期間一直開著的布林值。enter 和 exit 是一次性的脈衝,在動物安頓下來和起身時各播一次。variation 是一份一次性脈衝的清單,動物保持姿勢期間不時播一個,用的時鐘和待機變化一樣。只有 var 是必填的。

抽取是均勻的,每次進入休息都抽一次,所以兩項就是各 50%,動物也可能連續兩次抽到同一個。它不是輪流。

每個脈衝的時長由 idleAnimMs 對那個變數說了算,所以要傳表的形式,給每個變數一個長度,包括你已經有的待機變數。填片段的真實長度。

idleAnimMs = { cdIdle2 = 3800, cdIdle3 = 3800, cdSitIn = 1375, cdSitOut = 1417,
               cdSitGroom = 19833, cdSitGroom2 = 25167 },

片段和節點是你的,不是基礎模組的。每個變數都需要你附加模組裡 media/AnimSets/raccoon/idle/ 下的一個節點檔案,而它的 m_AnimName 指向的片段必須存在於你的模型裡。節點指向模型沒有的片段會靜默失敗,動物就只是站在基礎模組的待機裡。給姿勢節點的 m_ConditionPriority 設成 10,和基礎模組的一樣,要在它上面播的東西給更大的數:變化給 11,兩個過渡給 12。節點預設循環播放片段,除非你另外說明,所以每個只該播一次的節點,也就是兩個過渡和每一個變化,都需要 <m_Looped>false</m_Looped>。沒有它,片段一結束就重頭開始,動物會明顯地彈回去再做一遍,因為變數要到下一個伺服器 tick 才會關掉。全部只放在 idle/ 裡。放在 pathfind/ 裡的姿勢節點播的是沒有根運動的片段,會把動物定在原地。

聲音

沒有 voices 的話每個品種都用狗的聲音,所以貓會吠。先註冊你自己的音效:

CD.registerVoices({
    CDCatMeow = "bark", CDCatGrowl = "bark", CDCatHiss = "bark", CDCatPurr = "bark",
    CDCatPet = "fx", CDCatMeowAmbient = "ambient",
})

然後讓品種指向它們:

voices = { bark = "CDCatMeow", growl = "CDCatGrowl", idle = "CDCatPurr",
           wildbark = "CDCatMeowAmbient", pet = "CDCatPet", whine = "CDCatHiss" },

沒有 CD.registerVoices,聲音還是會播,但它們不理會玩家的分類滑桿和遊戲的音效音量,而且日誌裡沒有任何線索指向這件事。你在 voices 裡漏掉的鍵會退回狗的聲音。

whine 是動物受傷或生病時發出的聲音。eat 和 drink 是吃喝的音效。狗的是循環音,動物吃完時由基礎模組停止;如果你的也是循環音,把它們加進 CD.SOUND_LOOPED,聽者走出範圍時基礎模組也會把它們停掉。比 API 11 更舊的基礎模組會無視這三個鍵,照放狗的聲音。

第三個參數是每個音效的可聽範圍,以格計。它應該和你自己在音效腳本裡寫的 distanceMax 一致:

CD.registerVoices(map, nil, { CDCatMeow = 22, CDCatGrowl = 10 })

在伺服器上,基礎版只把聲音送給在那個範圍內的玩家。範圍不寫的話,這個聲音會拿到一個按分類給的通用範圍:一聲只傳十格的哈氣會送給三十格內的所有人。超過 distanceMax 之後, FMOD 的反比衰減會停止衰減而不是歸零,所以一個沒管好的 .ogg 會在任何距離上都以 distanceMin / distanceMax 的音量繼續播。同樣的道理,你那些大聲的音效要把 distanceMin 留小。

飲食

沒有 diet 的話每個品種都像狗一樣吃:一樣的食物會毒到牠,一樣的食物算肉,一樣的東西牠會從飼料槽自己吃。這個欄位只為你的品種覆寫那三張清單。

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 },
},

兩種寫法並存,是因為遊戲自己的分類只能做到這個程度。Cheese 是一個真的 FoodType,所以一個鍵就涵蓋了遊戲裡以及任何沿用這個分類的食物模組裡的每一種乳酪。鮪魚不是:開過的罐頭是 FoodType = Fish,和其他所有魚在一起,而沒開的罐頭根本沒宣告食物類型。 badParts 和 proteinParts 就是你搆到那些東西的辦法。

每個欄位都是一張 <key> = true(加入)或 <key> = false(拿掉基礎版本來有的)的表。不是 true 也不是 false 的值會被丟掉,並在日誌裡留下一行帶名字的記錄。

欄位 它的鍵是什麼
bad 會讓動物生病、讓牠掉忠誠度、在餵食選單裡顯示成紅色那些食物的 FoodType
badParts 物品類型的一小段,小寫,在整個類型字串裡任何位置比對
protein 能還掉動物肉債的 FoodType,也就是虛弱 moodle 盯著的那個
proteinParts 物品類型的一小段,比對方式和 badParts 相同
nonProtein 你確定不是肉的 FoodType。它只是讓基礎版對沒見過的食物類型寫的那行日誌閉嘴
trough 動物會自己從飼料槽吃的 FoodType 和 AnimalFeedType
troughItems 完整的物品類型,用在飼料槽裝得下、但本身沒有飼料類型的物品上

replace = true 從一張空清單開始,而不是從狗那張。當你的動物和狗幾乎沒有共通點時用它;一隻貓通常寫成幾個增減會比較省事。

那三張清單跟著動物走,所以一條狗和一隻貓站在同一個飼料槽前,吃到的東西不一樣。模組自己的食盆是例外:它存的是食物點數,不是把它裝滿的那個物品,所以裝填時對所有動物都走基礎版那張清單。反正有毒的東西本來就放不進食盆。

出生時的性別

每隻動物出現時都會抽一次公母,預設對半。maleChance 是生為公的比例,0 到 1,sterileMale 則把該品種的公排除在配種之外。

maleChance = 0.00033,
sterileMale = true,

這一對就是三花貓。橘色和黑色的花紋掛在 X 染色體上,所以三花幾乎都是母的,偶爾出現的公是 XXY,大約三千隻裡一隻,也生不出小貓。sterileMale 品種的公永遠不會被選為配偶,右鍵選單裡的配種選項也會消失。母的照常配種。

這種品種的公在遊戲顯示性別的地方都會帶上「不育」:狀態面板、右鍵選單、流浪動物掃描和狗舍,玩家就知道牠為什麼不配種。

兩個欄位都從品種定義裡讀,動物身上不寫任何東西,舊存檔也不用遷移。同一次抽籤也決定幼崽出生時的性別,所以在世界裡是母的品種,在窩裡也還是母的。

6. 整套關掉一個系統

五項技能就是五套系統:

技能 是哪套系統
scent 哨兵:發現殭屍並提醒你
combat 戰鬥、警戒模式和自衛
obedience 才藝樹
hunt 狩獵,加上牠給主人的採集加成
herding 照看圈裡的牲畜

所以一個物種宣告的是牠沒有什麼:

skills = { combat = false, herding = false },
canBreed = false,
huntMaxPrey = "tiny",

不寫或者寫 true 都表示這隻動物有那項技能。

關掉一項技能也會讓牠不再累積該項經驗,並且把那條進度條和它的指令從介面上移除。

canBreed = false 和那張表分開,因為配種既沒有進度條也沒有經驗。這隻動物不會懷孕,永遠不會被挑作伴侶,和自己同類也不配種。

物種

一個加入別種動物、而不是再加一種狗的擴充模組,要這樣宣告:

CD.registerSpecies({ key = "cat",
                     nounKey  = "IGUI_PD_SpeciesNoun_cat",
                     youngKey = "IGUI_PD_Young_cat" })

CD.registerBreed({ ..., species = "cat" })

到目前為止發佈過的每一個品種都沒有這個欄位,而沒有就表示 "dog"。

配種按物種封閉:只有雙方是同一物種才會懷孕,指令裡、被動的每小時抽取裡和除錯工具裡都一樣。介面也不再把你的動物叫做狗:nounKey 是填進"這隻%1已經屬於另一位倖存者"這類句子的光禿名詞,而 youngKey 是你這個物種的幼體在視窗、右鍵選單和犬舍裡帶的標籤,狗在那裡寫的是幼犬。

那兩個名詞只管得到繞著 %1 造的句子。其他每一句把狗這個字直接寫進去的句子("檢查動物"、"你的狗餓了"、"狗故意吠叫")從 API 7 起改用按物種的鍵:在你的翻譯檔裡放上 IGUI_PD_ScanStray_cat,只要畫面上的動物是貓,基礎版就會顯示它而不是 IGUI_PD_ScanStray。你沒放的鍵會退回狗的文字。接受這個後綴的鍵有:ScanStray、ScanTitle、 PauseGrowthTip、BadFood、AlertFullDesc、AlertQuietDesc、AlertLockedTip、HuntModeDesc、每一個 Trick*Desc、TrickDistractDone、TrickNeedsBag、TrickGotoPick、TrickSniffNoItem、 TrickFetchNoItem、AlertGroup、DogLost、每一個 Refused_*、KennelStatusNoData 以及照料類 moodle 的 Moodle_*_desc(Sick、Weak、Hunger、Thirst、Rested、Grief)。畫面上沒有動物的句子(登記分頁、"先和一隻動物成為朋友吧")在基礎版裡本來就是物種中立的。貓把它們十四種語言全部附上了;把牠的 IG_UI.json 當範本抄走。

動物的 group 只有除錯模式的生成選單和動作片段檢視器會讀,透過 IGUI_Animal_Group_<group>。在行為輔助函式之後設 t.group = "cat" 並附上那個鍵,否則你的物種在那裡會頂著狗的名字出現。

宣告了 species 卻沒註冊並不致命:你的動物照樣拒絕和自己物種以外的配種,因為那道關卡比對的是原始字串。只有那兩個名詞會退回狗的,而且日誌會指名說出來。

在比 API 6 更舊的基礎版上,這個欄位會被忽略而且不留日誌,你的動物會像狗一樣配種。如果牠絕對不能和狗雜交,就在 API 6 上設守衛,或者在基礎版還舊的時候保持 canBreed = false,貓就是這麼做的。

huntMaxPrey 限制的是一場仍然存在的狩獵裡的獵物體型,它不能取代 hunt = false。它接受 "tiny"(老鼠、大鼠、松鼠)、"small"(兔子、浣熊)或 "large"(鹿,預設值,不設上限)。你的動物不會去追、也不會去指認超過上限的獵物。不在這道階梯上的值會退回 "large",並在日誌裡印出一行帶名字的記錄。

被擋掉的選項會從輪盤、右鍵選單和視窗裡消失,伺服器也會不吭一聲地拒絕它。這些欄位沒有一個會新增翻譯鍵,所以請在你的品種描述裡把限制講清楚。

7. 動物定義

引擎把 mesh 綁在動物類型上,而不是品種上,所以你的品種需要三個自己的類型。最短的路是把某個現有擴充模組的檔案複製過來改名。

三個部分,放在 Definitions/animal/ 底下的同一個 shared 檔案裡:

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

基礎版給你的輔助函式:

輔助函式 幫你填什麼
CD.applyCompanionModel(t, "Body_Model") 身體模型、骨架、屠宰後的貼圖和動作集
CD.applyCompanionBehaviour(t) 一隻夥伴動物需要的每一個引擎行為旗標
CD.applyCompanionAvatar(t) 動物視窗用的肖像攝影機
CD.COMPANION_SOUNDS 共用的腳步聲和音效表
CD.defineGenome("species") 新物種的基因組,附帶標準基因清單(API 7)
CD.STRAY_NO_BLEEDOUT 讓受驚的流浪動物不會失血而死的值
CD.defineCompanionParts(typePrefix, engineBreed, meat) 三個階段的屠宰部位。meat 是選填:{ item, minNb, maxNb, pupMinNb, pupMaxNb },不填的話動物產出基礎版的狗肉。引擎會把每一塊的數量和飽足度都乘上屍體大小,所以體型數值大的物種(貓用 2.5 到 3.5 當視覺縮放)需要自己的物品,基礎飽足度要更小、數量要更低

物種中立的名字是 API 7 才有的。比較舊的那些,CD.applyDogModel、CD.applyDogBehaviour、 CD.applyDogAvatar、CD.DOG_SOUNDS、CD.defineDogParts 和 CD.DogMoodles,是同一批函式和表格頂著它們最早的名字。想在比 0.7.3 更舊的基礎版上載入的擴充模組,就對舊名字設守衛,新的存在時就挑新的: local applyModel = CD.applyCompanionModel or CD.applyDogModel。

動作集必須維持是 "raccoon"。分岔出去的動作集不會載入它的狀態機,動物會定在原地不動。

基因組要重複使用。狗的品種指向基礎版的清單:AnimalDefinitions.genome["dog"].genes。新物種呼叫一次 CD.defineGenome("cat"),然後把它回傳的表給每一個階段。引擎只讀每個階段的 genes 清單,而基因組的鍵只是個慣例,所以你的物種有自己的條目,卻不必有自己的基因清單。

不要宣告 mate。有了它,引擎會在任何動物區域裡跑它自己的原生交配,那套會忽略模組的配種封鎖和它的沙盒選項,生出來的幼犬沒有羈絆。基礎版在啟動時會清掉這個欄位,但不要靠它。

部位是必填的。三行,放在自己的檔案裡:

if CompanionDogs and CompanionDogs.defineDogParts then
    CompanionDogs.defineDogParts("pug", "pug")
end

沒有它們,屠宰你的動物會讓原版程式碼崩潰,因為它用類型和品種去索引部位定義,然後撞上 nil。

CD.applyDogAvatar 裡的肖像縮放是照著狗校準的,而頭像縮放是放大倍率:動物越小,數字越大。把縮放值除以牠相對狗的體型比例,再把偏移值乘上這個比例,否則你的動物會小小一隻縮在自己肖像的底部。

8. 模型腳本

media/scripts/ 底下的一個檔案告訴遊戲你的模型存在:

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 讓你模型檔案裡的動作片段保持可用。沒有它,模型會載入但永遠不會動。

head 這個掛點是帽子之類的東西待的地方。如果你想讓馱袋合到你的動物身上,就照同樣的方式在脊椎骨上加 saddlebags_l、saddlebags_r 和 saddlebags_c。這些數字要靠在遊戲裡看著動物一點一點挪出來。從模型上量出來的值會把袋子放在 mesh 的幾何中心,那不是它在身上看起來對的位置。

9. 這隻動物會在哪裡出現

流浪動物是按建築、按區塊抽取的,在那個區塊第一次載入的時候。你把自己的抽取寫成一串步驟,可以放在品種定義裡叫 spawns,也可以用 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 },
},
鍵 是什麼
id 這一步的名字,給你自己的日誌用
class 在哪一類建築上抽
chance 一個回傳百分比的函式,所以它能在抽取的那一刻讀到沙盒倍率。
suffix 持久儲存的鍵。在所有模組之間唯一,而且永遠不改。
breed 要生成的品種鍵
gate 選用函式,回傳真假,在抽取之前檢查。哈士奇用的是 CD.isWinter。
indoor 生在建築內部而不是街上的百分比機率。預設 35。

把你的機率乘上 CD.dogSpawnMultiplier(),或者把 CD.strayChancePerHouse() 除以你自己的稀有度。兩種做法都能讓玩家的沙盒設定繼續有效。

indoor 只是個偏好:那個區塊裡內部沒有空格子的建築會退回到外面的格子。

建築類別

基礎版註冊了四個:

類別 匹配什麼
house 不是商店的住宅建築
police 警察局之類
petvet 寵物店和獸醫診所
farm 農場建築,而且允許出現在城鎮之外

如果沒有一個合適,就註冊你自己的:

CD.registerBuildingClass("mineshaft", function(def) return def:isX() end,
                         { skipUrbanGate = true })

匹配函式收到建築定義,並且執行在一次保護呼叫裡,所以它裡面出錯不會中斷區塊載入。 skipUrbanGate 讓這個類別可以在非市區的區塊裡抽取,農場或者林中小屋需要這一點。不要設 exclusive。基礎類別彼此互斥,擴充模組的類別是疊在它們之上匹配的。

已經被佔用的後綴

後綴是「這棟建築已經抽過了」這筆記錄的鍵的一部分。兩個模組用同一個後綴會互相破壞對方的抽取結果。

每個後綴都以一條直線開頭。已經在用的有:

  • 基礎版:空後綴,還有 g、h、hv、bc、gh、hh、bh
  • 羅威納犬:rw、r
  • 杜賓犬:db、dm、dh
  • 拉布拉多:lb、lk
  • 巴哥犬:pg、pv
  • 馬林諾斯犬:ml、mm、mh
  • 貓:以 ct、cv 或 cs 開頭的每一個後綴(每種毛色一組)

10. 品種 moodle

moodle 是一個客戶端檔案。你往 CD.DogMoodles 裡追加一張表,剩下的繪製、追蹤和清理都由基礎版負責:

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 回傳等級,0 表示「不顯示」。apply 收到距離上次呼叫過去的遊戲分鐘數,所以要把你的效果乘上 elapsedMin。

圖示要提供六種尺寸,從 32 到 128 像素。介面按玩家的縮放挑尺寸,缺一個尺寸就會顯示成一個空方塊。圖示出現在模組自己那一條裡,不和原版 moodle 混在一起。

11. 掛鉤

兩個函式清單,基礎版在伺服器上呼叫它們。從一個 shared 檔案往裡追加。每個處理函式都跑在自己的保護呼叫裡,所以一個擴充模組出錯不會中斷基礎版。

CD.onHuntDelivered 從 API 2 開始有,在動物把獵物放到主人腳邊之後立刻被呼叫:

CD.onHuntDelivered[#CD.onHuntDelivered + 1] = function(animal, owner)
    -- server side, so this is the right place to write mod data
end

CD.onUpkeepStress 從 API 3 開始有,每個照料週期都會被呼叫,在基礎版加完未滿足需求帶來的壓力之後、寫入之前。回傳一個數字,它會被加進總數:

CD.onUpkeepStress[#CD.onUpkeepStress + 1] = function(animal, needsStress)
    return CD.pugHeatRatio(animal) * CD.PUG_HEAT_STRESS_MAX
end

照料週期每一輪都會重算並覆寫這個值,所以從外面寫進去的東西下一個 tick 就沒了。掛鉤適合用在來自環境的條件上,比如炎熱、寒冷或者下雨。

在多人模式裡,客戶端永遠不寫 mod data。客戶端這邊的一次寫入會在下一次同步時覆寫伺服器的副本,而症狀要過幾分鐘才在一個不相干的地方冒出來。請在伺服器檔案裡做這件事,或者用這些掛鉤。

SERVER TUNING面板裡的數字都是普通的 CD.* 常數,附加模組可以在載入時從 shared 或伺服器檔案裡給其中一個賦值。從 API 9 起,面板把伺服器載入完成那一刻生效的值當作那個旋鈕的預設值:它一直保持到管理員手動改寫,重設按鈕回到它,面板也把它顯示為預設值。在更舊的基礎模組上,面板每次啟動都會把出廠值寫回去。要在載入時賦值,不要在之後的事件裡賦,否則下一次改動任何旋鈕都會把出廠值放回來。

12. 翻譯鍵

每個語言資料夾放一個 IG_UI.json,位置在 media/lua/shared/Translate/<LANG>/。基礎版帶了十四種:EN、PTBR、CH、CN、DE、ES、FR、IT、JP、KO、RU、TH、UA、VI。

一個品種需要的鍵:

鍵 顯示在哪裡
IGUI_PD_Breed_<key> 品種名,模組介面裡到處都用
IGUI_PD_BreedDesc_<key> 動物卡片上的描述
IGUI_Breed_<engineBreed> 遊戲自己那些畫面裡的品種名
IGUI_AnimalType_<type> 你那三個類型各一個
你的物種鍵 你傳給 CD.registerSpecies 的 nounKey 和 youngKey
你的 moodle 鍵 名字和描述
{
    "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."
}

翻譯字串裡的裸百分號會讓顯示它的那個畫面崩潰,因為格式化器把它當成佔位符讀。帶位元組順序記號的語言檔案無法編譯,而且報錯指的是另一個檔案,不是壞掉的那個。

13. 存檔記住的東西

你的三個識別碼會被寫進存檔檔案,並且永遠被讀回來:

  • key,存在你每一隻動物身上的品種識別碼
  • typePrefix,它組成引擎保存的那些動物類型
  • suffix,「已經抽過」這份儲存的鍵,每個生成步驟一個

這三個在發佈之後都不能改名。改 key 會讓那個品種所有活著的動物變成孤兒。改 suffix 會讓所有現存存檔裡的每一棟建築重新抽一次,玩了幾個月的世界會塞滿你的動物。

新後綴同時也是新品種進入已經在跑的世界的辦法。CD: Cats 給牠四種新毛色配了新後綴,牠們於是開始在現有存檔裡出現,而原來那隻黑貓保留舊後綴,沒有再抽一次。

從還有該品種活體動物的存檔裡移除擴充模組,會把牠們弄丟。引擎會丟掉一隻牠不再認識類型的動物。基礎版不會崩潰,羈絆和犬舍都會乾淨地降級。請在你的 Workshop 頁面上說明這一點。

如果你的擴充模組缺席,基礎版對任何被問到的東西都退回預設品種,所以沒裝你的模組打開的存檔仍然可讀。

14. 發佈之前

  • 守衛在每一個 Lua 檔案裡,而且寫的是你實際用到的那個 API。
  • 基礎版的版本下限用文字寫在你的描述裡。versionMin 說不了這件事。
  • 你在第一次載入時讀了日誌。否則 CD.registerBreed 會默默拒絕,而每一次拒絕都會印出一行帶名字的記錄。
  • CD.defineDogParts 呼叫了。屠宰一隻你的動物,看看牠不會崩。
  • 你的 key、typePrefix 和生成後綴都是唯一的,而且你不會需要改它們。
  • 小型品種設定了 puppySize,而且你親眼見過一隻幼犬來證明這一點。
  • 如果是別的物種,CD.registerVoices 呼叫了而且 voices 填了,裡面也有 whine,不然牠會像狗一樣又吠又嗚咽。
  • moodle 圖示六種尺寸都在。
  • 你提供的每一個語言資料夾都有每一個鍵。缺一個鍵,畫面上就會顯示原始鍵名。
  • 你在多人模式裡測過,不只是單人。位置、歸屬和 mod data 在那邊的表現都不一樣。
  • 你的 Workshop 頁面警告了不要從還有活體動物的存檔裡移除擴充模組。

15. 常見錯誤

症狀 是怎麼回事
"我的動物從來不出現。" 幾乎總是守衛:附加模組需要一個已安裝基礎模組沒有的 API,於是它提前返回,什麼都沒註冊。
"牠出現了,但僵住不動。" 動畫組不是 "raccoon",或者模型缺了片段。
"牠算繪成一團黑,日誌刷個不停。" 模型的骨骼超過 60 根。
"宰牠的時候崩潰。" 沒有呼叫 CD.defineDogParts。
"幼崽出生就是成年體型。" 小體型品種沒有設定 puppySize。這個值是絕對值,而且每一輪掃描都會重新套用,所以該型別的尺寸區間蓋不住它。
"我的兩個品種老是互相變來變去。" 牠們共用了一個 engineBreed。字首選的是網格,同一家族可以共用。而 engineBreed 選的是貼圖,用來區分這個家族裡的各個品種,所以每個品種都必須唯一。
"聲音放得出來,卻不理會音量滑桿。" 那個聲音沒有呼叫 CD.registerVoices。
"單人能用,上伺服器就出毛病。" 有東西在客戶端寫 ModData。把它挪到伺服器端。
"我明明關掉了戰鬥,牠還是和殭屍打起來。" 牠執行所在的基礎模組比 API 5 更舊。那個基礎模組不認識這個欄位,會直接忽略。守衛應當在這個基礎模組上攔住載入,而不是缺著欄位照樣載入。

這份手冊裡有哪裡寫錯了,或者少了什麼?到 Discord說一聲。