最後更新於 2022 年 6 月 20 日
前言
本篇面向 完全不懂 JS 、從沒接觸過程式語言的小白,因此廢話極多,有水平的請繞道。
楓之谷的 NPC 腳本是使用 javascript 去寫的,如果你想要經營一個私服,你可以不懂 js 是怎麼寫的沒關係,畢竟網路上資源一抓一大把,但如果你想要自己添加新的內容或者修改成你想要的效果,不懂一點 js 是很難去寫腳本的。
在這邊我提供幾個學習 js 的網站:
- https://www.runoob.com/js/js-tutorial.html
- https://javascript.info/
- https://www.w3schools.com/js/js_intro.asp
本篇內容主要是放在「寫」NPC 腳本的部分,至於要怎麼去改,需要用什麼工具請參考我之前發的文:
腳本架構
首先請你將 伺服器端\Libs\scripts\npc\10200.js
用文字編輯器打開,你會看到有兩個 function 打頭的程式碼:
function start() { action(1, 0, 0); } function action(mode, type, selection) { // code }
這個以 function
打頭的東西是什麼?
這是 函式。
函式是構成JS的基本要素之一,一個函式本身就是一段JS程序,我們可以用它來執行於某一個任務或計算。
一個函式的定義由一系列的函式關鍵詞組成,依次為:
- 函式的名稱。
- 包圍在括號()中,並由逗號區隔的一個函式參數列表。
- 包圍在大括號{}中,用於定義函式功能的一些JavaScript語句。
// action 為函式名稱 // 參數為 mode, type, selection function action(mode, type, selection) { // code }
大概理解了函式的意思,就可以來解讀這段程式碼的涵義了:
function start() { action(1, 0, 0); // 調用 action方法,並且依次傳入 1, 0, 0 對應到action函式的參數 mode, type, selection } function action(mode, type, selection) { // mode = 1 // type = 0 // selection = 0 }
通常來說,會需要「做事」的NPC都是需要使用一個變數 status
來控制。
var status = -1; //1. 將 status 預設為 -1 function start() { action(1, 0, 0); } function action(mode, type, selection) { if (mode == 1) { // 2. 當 start()一調用action(1, 0, 0)時, mode為1 // 因此 status 會增加 , 此時 status 為 0 status++; } else { if (status == 1) { cm.sendNext("如果你想體驗弓箭手的感覺,再來跟我對話。"); cm.dispose(); return; } status--; } if (status == 0) { // 3. status = 0 , 因此會執行這個判斷式中的敘述 cm.sendNext( "弓箭手有靈敏與力量的支援,主要負責長途攻擊,為前線的戰鬥者提供支援。非常擅長使用弓,作為攻擊的一部分。" ); } else if (status == 1) { cm.sendYesNo("你想體驗一下弓箭手的感覺嗎?"); } else if (status == 2) { cm.MovieClipIntroUI(true); cm.warp(1020300, 0); cm.dispose(); } }
action()
其中這段程式碼可以這麼理解:
- 在與NPC對話時,按 OK、下一頁、接受…等時
mode = 1
,status
增加 1; - 點了否、拒絕
mode = 0
,status
減少 1; - 若是停止對話,
mode = -1
,就會讓status
減少 1。
if (mode == 1) { status++; } else { if (status == 1) { cm.sendNext("如果你想體驗弓箭手的感覺,再來跟我對話。"); cm.dispose(); return; } status--; }
if else 是什麼?請參考:https://developer.mozilla.org/zh-TW/docs/Learn/JavaScript/Building_blocks/conditionals,就不多提了。
當然,並不是所有腳本的架構都長成上面那樣,全看你的NPC需要做到什麼功能。
比如什麼事情都不需要做只需要說說話(不是)的NPC腳本如下:
function start() { cm.sendOk("Hello."); cm.dispose(); }
有的還只有 action 函式( 1002005.js
)
function action(mode, type, selection) { cm.sendStorage(); cm.dispose(); }
基本NPC方法
對話框
cm.sendSimple("內容"); // 顯示對話框 內容 cm.sendNext("內容"); // 顯示對話框 下一頁 cm.sendOk("內容"); // 顯示對話框 OK cm.sendNextPrev("內容"); // 顯示對話框 上一頁和下一頁 cm.sendYesNo("內容"); // 顯示對話框 是 和 否 cm.sendAcceptDecline("內容"); // 顯示對話框 接受 和 取消 cm.sendGetNumber("內容",1010001,1010001,1012672); // 顯示對話框 接收玩家輸入的數值 (初始值 1010001, 最小值 1010001, 最大值 1012672) cm.dispose(); // 關閉 NPC 對話
其他常用功能
cm.warp(1020300, 0); // 將玩家傳送至地圖 1020300 cm.haveItem(itemid); // 玩家是否擁有 itemid 道具 cm.gainItem(itemid, 1); // 給予玩家 itemid 1 個 (如果要從玩家身上拿取 則將 1 改為 -1) cm.getPlayer().getName(); // 獲取玩家id cm.getPlayer().getMeso(); // 獲取玩家楓幣 cm.getPlayer().itemQuantity(4001126); // 獲取玩家擁有 4001126 物品的數量 cm.getPlayer().getCSPoints(1); // 1:獲取玩家擁有的GASH點數 2:楓葉點數 cm.getPlayer().getLevel(); // 獲取玩家等級 cm.canHold(itemid); // 判斷該玩家背包是否有空閒能獲取該道具 cm.getMapId(); // 獲取當前地圖id cm.isLeader(); // 玩家是否為隊伍隊長 cm.openShop(shopid); // 開啟shopid 商店
什麼是cm?
如果有看過 NPCHandler.java
就會知道為什麼在撰寫NPC腳本的時候,使用方法前面都要加上一個 cm
。
因為在 handling\channel\handler\NPCHandler.java
的 NPCMoreTalk()
方法中,有這麼一句程式碼:
final NPCConversationManager cm = NPCScriptManager.getInstance().getCM(c);
如果沒有JAVA基礎的話這看起來和天書沒什麼兩樣。
這裡的 NPCConversationManager 代表 NPCConversationManager.java
中的 NPCConversationManager
類別,與 NPC 對話相關的方法都會寫在裡面,比如 sendYesNo()、sendOk()、dispose()…等。
你只需要將這句程式碼理解成:我可以透過 cm
去調用 NPCConversationManager.java
中的方法 就好。
比如我想要使用一個 OK 的對話視窗,就可以寫成:
cm.sendOk("內容");
這只是一個補充,不懂也沒事。
對話內容
對話內容字體和顏色
- #b – 藍色
- #d – 紫色
- #g – 綠色
- #k – 黑色
- #r – 紅色
- #e – 粗體
- #n – 正常 (移除粗體)
用法:
cm.sendOk("#b 這邊是藍字 , #k 這邊變黑字");
其他
以下 [] 用於區隔,並不是真的要打上 []
- #c[道具ID]#-顯示玩家背包中有多少
- #h # – 顯示玩家名稱
- #m[地圖ID]# – 顯示地圖名稱
- #o[怪物ID]# – 顯示怪物名稱
- #p[NPC ID]# – 顯示 NPC 名稱
- #q[技能ID]# – 顯示技能名稱
- #s[技能ID]# – 顯示技能圖片
- #t[道具ID]# – 顯示道具名稱
- #i[道具ID]# – 顯示道具圖片
- #i[道具ID]:# – 顯示道具圖片+滑鼠移動圖片顯示
- #z[道具ID]# – 顯示道具名稱+詳細資料
- #v[道具ID]# – 顯示道具圖片
- #B[%]# – 顯示進度條
- #f[圖片位址]# – 顯示 WZ 檔案中的圖片
- #F[圖片位址]# – 顯示 WZ 檔案中的圖片
用法:
cm.sendOk("道具圖示:#i4001126#,道具資訊:#z4001126#");
格式
- \r\n – 換行
- \r – 回車
- \n – 新行
- \t – TAB(4個空格)
帶有選項的NPC
![[楓之谷私服] NPC腳本詳細寫法 se1 1 se1 1 [楓之谷私服] NPC腳本詳細寫法](https://namepluto.com/wp-content/uploads/2021/06/se1-1.png)
在對話中列出選項:
#L0# 選項文字 #l
- 0 是可以隨機定義的數,不一定要從 0 開始,並且注意數字不能重複,會出錯。
- 點擊選項後 status 會增加 1 => 點選項一後 status = 0 變成 status = 1
cm.sendSimple("你好,請選擇選項。\r\n #L0# 我要選擇選項一 #l \r\n #L1# 我要選擇選項二 #l \r\n #L2# 我要選擇選項三 #l");
![[楓之谷私服] NPC腳本詳細寫法 se2 1 se2 1 [楓之谷私服] NPC腳本詳細寫法](https://namepluto.com/wp-content/uploads/2021/06/se2-1.png)
接著會需要使用到 action 函式的 selection
參數,我們需要宣告一個變數,將 selection 賦予給該變數,然後使用這個變數來做條件判斷(switch case)。
var sel = selection; // 接收玩家點選的選項值 switch (sel) { // 做條件判斷 case 0: // 選了 L0 之後做的事 cm.sendNext("你選擇了選項一!"); break; case 1: // 選了 L1 之後做的事 cm.sendNext("你選擇了選項二!!"); break; case 2: // 選了 L2 之後做的事 cm.sendNext("你選擇了選項三!!!"); break; default: break; }
注:每個case 之間互不影響,case最後都需要加上一個 break; 離開 switch 區塊。
完整寫法
var status = -1; function start() { action(1, 0, 0); } function action(mode, type, selection) { if (mode == 1) { status++; } else { status--; cm.dispose(); } if (status == 0) { // 點擊選項後 status 會增加 1 => 點選項一後 status = 0 變成 status = 1 cm.sendSimple( "你好,請選擇選項。\r\n #L0# 我要選擇選項一 #l \r\n #L1# 我要選擇選項二 #l \r\n #L2# 我要選擇選項三 #l" ); } else if (status == 1) { var sel = selection; // 接收玩家點選的選項值 switch ( sel // 做條件判斷 ) { case 0: // 選了 L0 之後做的事 cm.sendNext("你選擇了選項一!"); break; case 1: // 選了 L1 之後做的事 cm.sendNext("你選擇了選項二!!"); break; case 2: // 選了 L2 之後做的事 cm.sendNext("你選擇了選項三!!!"); break; default: break; } } else if (status == 2) { cm.dispose(); } }
學到這裡,大部分的NPC腳本應該都能看懂了,如果還想要更深入的話可以去學一下javascript基礎,其實不是很難的。
我自己有寫了幾個常見的NPC腳本,有興趣想參考的可以看一下:
延伸閱讀:
NPC Scripting, What You Can Do To Shorten Your NPC Scripts!
Ultimate NPC Scripting
非常詳細的教學 謝謝
大大你好,我想使用您最後一種模式寫萬能NPC但遇到一點困難
我將9010000的楓之谷GM設定為萬能NPC
新增了一個選項想要用cm.openNpc的方式連結到另一個NPC
但在連結的過程會顯示角色狀態異常,要用@ea解卡
實在找不到該怎麼處理,希望大大知道的話可以提點一下QQ
沒事了XD 發現少一個dispose 感恩大大
大大你好,我這幾天遇到龍王沒辦法換地圖的情況
傳點按上一直顯示這句話
Horntail’s Seal is Blocking this Door.
我用HaCreator找地圖240060000檢查有問題的傳送點
有看到他對應的portal是next00但沒有找到
只有找到下面那個hontale_BR
我用notepad打開hontale_BR.js 還是看不出個所以然…
如果大大知道怎麼解的話,麻煩您指點一下
補圖
伺服器端/Libs/scripts/portal/hontale_BR.js 裡面找到判斷式 if (pi.getPlayer().getMapId() == 240060000)
因為這個 var avail = eim.getProperty(“head1”); 回傳的是 no 所以 return false 造成沒辦法傳送到下一個地圖。
看著是這裡的 head1, head2 沒有正確檢測到龍頭已經死亡,有一種解決方式是改為判斷當前地圖是否還有怪物活著,如果沒有就可以傳送到下一個地圖,程式碼:點我下載 hontale_BR.js
但這樣的弊端是,萬一龍頭還沒召喚出來就直接過傳送門應該是可以過去的(不過我到傳送門的速度總是比怪物召喚的還慢XD所以我也不太確定)
謝謝大大的回覆,我剛剛看到大大說回傳的是no所以沒辦法傳送
不知道哪根筋不對把if (avail != “yes”) 我把yes改成no就變成可以傳送
只是龍頭在也能硬闖
另外大大的程式碼沒辦法下載qq
avail 這個是用來判斷龍頭死了沒的, 改成 != yes 就會變成直接放行XD
試試看用這個下載: https://ufile.io/eyspjav2
點左邊的 free download
我找到了別人的完美解決方式,不需要去改 portal/hontale_BR.js 和判斷地圖上還有沒有怪物,只需要簡單改一下 Event/HorntailBattle.js 就好:https://reurl.cc/6ZRGoV
感恩大大…原來跟portal/hontale_BR.js無關
剛剛用GM帳號測試了一下
如果是用指令 !killall 殺掉龍頭的話
系統一樣會沒偵測到龍頭死了
必須先攻擊他一下才能用指令殺掉
這樣才會偵測到龍頭死了然後放行