到目前為止,我們已經可以和 NPC 對話接任務,以及撿拾藥草到背包了,剩下最後一片拼圖:可以和 NPC 對話完成任務。
預想的流程是這樣:
使用者在 P 四周按空白鍵開啟對話
清除地圖,顯示對話
村民:
年輕人,你回來啦?狀況如何?
你:
A: 我拿到 3 個草藥了。
B: 我還沒去。
當使用者按 B 結束對話
當使用者按 A 顯示下一段對話
村民:
太感謝你了,這是我自己熬煮的雞湯,你帶著路上喝吧。
你:
A: 謝謝你。
使用者按 A 結束對話
雖然這個 RPG 任務的練習其實就只有到完成任務為止,但是因為我寫到這邊覺得通常遊戲裡到這邊應該會送點小東西?或者經驗值提升?
所以我在這裡寫了 NPC 會贈送雞湯給玩家,但是這就遇到另外一個問題:要怎麼顯示背包的內容有那些東西呢?
不過呢,一如我們一直在練習的:要問自己這是不是今天的任務呢?
今天的主要目的是要完成整個 RPG 遊戲到可以跑完我們先前訂下的目標,因此我們先不深入研究這個問題,先把這個問題記下來,明天我們還有一個篇幅的時間,我們就可以一起來解決這個問題。
感覺似乎需要延續上一次設計的 NPC 狀態:
其實因為我想走最短路徑完成這個功能,所以目前我想先用這個方式來完成,但我內心總覺得它不是一個最適合的架構,如果有多個任務或是隨遊戲進展有不同的階段顯示不同的對話,該怎麼設計?
這個問題就一樣先記下來,不過由於篇幅的關係,這個系列中就不會去討論,留給大家自己來思考囉。
先補上狀態機設計圖:我只放後半部

這邊因為是已經寫過的東西,我就不拆解目標,直接動手。
首先新增變數:
const thirdTalk = [
'村民:',
'年輕人,你回來啦?狀況如何?',
'',
'你:',
'A: 我拿到 3 個草藥了。',
'B: 我還沒去。'
]
const fourthTalk = [
'村民:',
'太感謝你了,這是我自己熬煮的雞湯,你帶著路上喝吧。',
'',
'你:',
'A: 當然可以。',
'B: 抱歉,我趕時間。'
]
接著先處理按下空白鍵之後的邏輯,如果是未開始或者是已接下任務的話,就可以按空白鍵打開對話。
//在 openNPCTalk 內
if (isNext && (npcStatus === 'notStarted' || npcStatus === 'getTask')) {
// 是的話就清除畫面
process.stdout.write(`\x1b[${viewHeight}A`); // 上移游標
process.stdout.write('\x1b[J'); // 清除畫面
process.stdout.write("\n".repeat(firstTalk.length));//推出空間
// 顯示對話畫面
let frame = '';
if (npcStatus === 'notStarted') {
npcStatus = 'firstTalk';
for (let i = 0; i < firstTalk.length; i++) {
frame += firstTalk[i] + '\n'
}
process.stdout.write(`\x1b[${firstTalk.length}A`);//上移游標
} else if (npcStatus === 'getTask') {
npcStatus = 'thirdTalk';
for (let i = 0; i < thirdTalk.length; i++) {
frame += thirdTalk[i] + '\n'
}
process.stdout.write(`\x1b[${thirdTalk.length}A`);//上移游標
}
process.stdout.write(frame);//印出
}
然後去處理按下 A 和 B 的監聽,把新加入的狀態加進來做判斷:
// 按下 A 按鍵
if ((key === 'a' || key === 'A') && (npcStatus === 'firstTalk' || npcStatus === 'secondTalk' || npcStatus === 'thirdTalk' || npcStatus === 'forthTalk' )) {
pushNPCStatus();
return;
}
// 按下 B 按鍵
if ((key === 'b' || key === 'B') && (npcStatus === 'firstTalk' || npcStatus === 'secondTalk' || npcStatus === 'thirdTalk')) {
npcStatus = 'notStarted';
// 按下 B 後回到遊戲畫面
process.stdout.write(`\x1b[${firstTalk.length}A`);
process.stdout.write('\x1b[J');
process.stdout.write('\n'.repeat(viewHeight));
render();
return;
}
最後是處理按下 A 之後的這個 pushNPCStatus 的邏輯,加入第三段對話切換到第四段對話,以及第四段對話完成的邏輯,其實都是複製前面的下來改。
else if (npcStatus === "secondTalk") {
npcStatus = "getTask";
task += 1;
// 清除畫面
process.stdout.write(`\x1b[${secondTalk.length}A`); // 上移游標
process.stdout.write('\x1b[J'); // 清除畫面
process.stdout.write('\n'.repeat(viewHeight));//推出空間
render();
} else if (npcStatus === "thirdTalk") {
npcStatus = "forthTalk";
// 清除畫面
process.stdout.write(`\x1b[${thirdTalk.length}A`); // 上移游標
process.stdout.write('\x1b[J'); // 清除畫面
process.stdout.write("\n".repeat(fourthTalk.length));//推出空間
// 顯示對話畫面
let frame = '';
for (let i = 0; i < fourthTalk.length; i++) {
frame += fourthTalk[i] + '\n'
}
process.stdout.write(`\x1b${fourthTalk.length}A`);//上移游標
process.stdout.write(frame);//印出
} else if (npcStatus === "forthTalk") {
npcStatus = "complete";
task -=1;
// 清除畫面
process.stdout.write(`\x1b[${fourthTalk.length}A`); // 上移游標
process.stdout.write('\x1b[J'); // 清除畫面
process.stdout.write('\n'.repeat(viewHeight));//推出空間
render();
}
比較容易忘記的是這邊要將第二段對話按 A 之後的狀態改為 getTask ,然後加上兩段新的邏輯。
到這邊我們完成整個對話的切換了,但還沒有完成檢查有沒有草藥和扣除草藥的邏輯,我們還是先印出來檢查切換正不正常:
現在切換流程是正常的,而且也已經在側邊欄將任務數字扣除了,但是我們要在扣掉任務之前,先做判斷,這是我們前面沒有擬到的流程,我們要先停下來想一想希望怎麼呈現這個部分:
感覺一般線上遊戲都是直接不顯示 A 回答?
所以如果要補充狀態機會怎麼做呢?

我試著調整成這樣,可能不是正規的繪製方法,但我想重點在於試著利用這個方式去釐清自己的想法。
那麼就來實作判斷之後決定要不要印出回答的邏輯
else if (npcStatus === 'getTask') {
npcStatus = 'thirdTalk';
for (let i = 0; i < thirdTalk.length; i++) {
if (bag < 3 && i === 4){
continue;
}
frame += thirdTalk[i] + '\n'
}
測試看看
發現有成功隱藏,但是 A 還是可以按,所以我們要阻止按 A,順便這時候把背包的草藥扣三。
// 在 pushNPCStatus 內的這一段
else if (npcStatus === "thirdTalk") {
// 加上這個
if (bag < 3){
return;
}
npcStatus = "forthTalk";
bag -= 3 ; // 扣掉三
// 後面省略
}
另外我也發現 render 需要加上第三個對話和第四個對話時阻擋重繪地圖。
if (npcStatus === 'firstTalk' || npcStatus === 'secondTalk' || npcStatus === 'thirdTalk' || npcStatus === 'forthTalk') {
return;
}
來玩一次整個遊戲看看吧!!
確定可以跑完一遍整個流程了!雖然說我自己多嘗試了幾次發現有一些 bug ,像是某些部分的重新渲染沒有被阻擋到之類的,但是呢,今天的工作邊界還是要踩穩!先完成到這邊, debug 的部分就交給未來的自己。
你還有觀察到哪些問題呢?試著想想該怎麼解決吧!明天見囉。
如果你想要瀏覽完整的程式碼,可以參考這裡:第 28 天程式碼