diff --git a/packages/cli/cli.js b/packages/cli/cli.js index 91b5d66..6cf99f0 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -1,3 +1,4 @@ +/* eslint-disable no-await-in-loop */ /* eslint-disable no-restricted-syntax */ const fs = require('fs').promises const { exit } = require('process') @@ -5,6 +6,24 @@ const readline = require('readline') const RETURN_SYMBOL = '\u21B5' +// ANSI color map +const ANSI_COLORS = { + black: 30, + red: 31, + green: 32, + yellow: 33, + blue: 34, + magenta: 35, + cyan: 36, + white: 37, +} + +function colorize(content, color) { + const colorCode = ANSI_COLORS[color] + if (!colorCode) return content + return `\x1b[${colorCode}m${content}\x1b[0m` +} + process.stdin.setRawMode(true) const rl = readline.createInterface({ @@ -19,13 +38,14 @@ function waitForEnter() { }) } -function printResponse(response, delay) { +function printLine(content, delay, color) { + const printContent = `${colorize(content, color)}${RETURN_SYMBOL}` return new Promise((resolve) => { let i = 0 const timer = setInterval(() => { - process.stdout.write(response[i]) + process.stdout.write(printContent[i]) i += 1 - if (i >= response.length) { + if (i >= printContent.length) { clearInterval(timer) waitForEnter().then(resolve) } @@ -33,7 +53,7 @@ function printResponse(response, delay) { waitForEnter().then(() => { clearInterval(timer) - process.stdout.write(response.slice(i)) + process.stdout.write(printContent.slice(i)) process.stdout.write('\n') resolve() }) @@ -42,8 +62,18 @@ function printResponse(response, delay) { async function printResponses(responses, delay = 100) { for (const response of responses) { - // eslint-disable-next-line no-await-in-loop - await printResponse(response + RETURN_SYMBOL, delay) + const { type, content, speaker, color } = response + if (type === 'narrative') { + for (const line of content) { + await printLine(line, delay, color || 'white') + } + } + if (type === 'dialogue') { + console.log(`${speaker}:`) + for (const line of content) { + await printLine(`\t${line}`, delay, color || 'green') + } + } } rl.close() } @@ -88,11 +118,14 @@ async function main() { }) const data = await res.json() if (data.responses) { - await printResponses(data.responses, 100) + await printResponses(data.responses, 60) + } + if (data.end) { + console.log(colorize('Quest completed!', 'blue')) } } catch (err) { console.error(err) } } -main().then(exit) +main().then(exit).catch(console.error) diff --git a/server/quests/helloworld/game.yml b/server/quests/helloworld/game.yml index e7dd507..2dca81c 100644 --- a/server/quests/helloworld/game.yml +++ b/server/quests/helloworld/game.yml @@ -10,13 +10,20 @@ stages: command: - echo Hello World! responses: - - 這個世界是在 3560000 時脈以前,由初代王建立了 Unix 律法,所形成的世界。然而在經過了好幾世代的王後,終於誕生了永恆之王——薩加(Saga),她比歷代的君王都更為強大,且從來不會衰老,使得這個世界變得前所未有的強大。 - - 然而,外在世界一直不斷在改變,同時在面臨外敵的不斷攻擊,而薩加卻始終用同一套治理的手段。最終,在 160000 時脈前的一次入侵中,根部遭到攻陷,薩加遭到封印,而這個國家也陷入了黑暗。 - - 由於長久以來依賴於薩加,世人已失去了「指令」的力量,即將走向衰敗和滅亡的命運。 - - 你的任務,就是找回被世人遺忘的指令,奪回 Linux 之根,成為 Linux 之王。 - - 首先,你必須要學會如何使用指令。 - - 一個指令是由一個指令名稱和一些參數所組成的,而指令名稱和參數之間是以空白字元分隔。 - - 例如,你可以輸入 `ls` 來查看目前的目錄下有哪些檔案。 + - type: narrative + content: + - 你聽到了一個聲音: + - type: dialogue + speaker: Unix Spirit + content: + - 你好,指令官,我是你的 Unix 精靈。我已在此恭候多時。 + - 這個世界是在 3560000 時脈以前,由初代王建立了 Unix 律法,所形成的世界。然而在經過了好幾世代的王後,終於誕生了永恆之王——薩加(Saga),她比歷代的君王都更為強大,且從來不會衰老,使得這個世界變得前所未有的強大。 + - 然而,外在世界一直不斷在改變,同時在面臨外敵的不斷攻擊,而薩加卻始終用同一套治理的手段。最終,在 160000 時脈前的一次入侵中,根部遭到攻陷,薩加遭到封印,而這個國家也陷入了黑暗。 + - 由於長久以來依賴於薩加,世人已失去了「指令」的力量,即將走向衰敗和滅亡的命運。 + - 你的任務,就是找回被世人遺忘的指令,奪回 Linux 之根,成為 Linux 之王。 + - 首先,你必須要學會如何使用指令。 + - 一個指令是由一個指令名稱和一些參數所組成的,而指令名稱和參數之間是以空白字元分隔。 + - 例如,你可以輸入 `ls` 來查看目前的目錄下有哪些檔案。 hints: - 使用 `ls` 指令來查看目前的目錄下有哪些檔案。 next: ls @@ -25,5 +32,44 @@ stages: command: - ls responses: - - NOTHING + - types: narrative + content: + - 一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡,房間裡有一個門,門上有一個鎖。 + - 你發現了一個檔案,檔名是 `forgotten_scroll.txt`。 + - type: dialogue + speaker: Unix Spirit + content: + - 你找到了一個被遺忘的卷軸,上面寫著解除封印的咒語。 + - 使用指令 `cat [檔案名稱]` 來查看檔案內容。 + - 我可以為你提供一些指令的提示,只要輸入指令的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。 + - 例如,你可以輸入 `ca` 然後按下 `Tab` 鍵,就可以看到 `cat` 這個指令的提示了。 + - 檔案名稱也一樣,只要輸入檔案名稱的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。 + hints: + - 使用 `cat [檔案名稱]` 指令來查看檔案內容。 + - 使用 `Tab` 鍵來取得指令提示。 + next: cat + - id: cat + condition: + command: + - cat forgotten_scroll.txt + responses: + - type: dialogue + speaker: Unix Spirit + content: + - 你找到的是一個被遺忘的卷軸,上面寫著解除封印的咒語。 + - 使用你已經知道的 `echo` 指令來詠唱咒語。 + next: echo + - id: echo + condition: + command: + - echo Unix, rise and guide me! + responses: + - type: narrative + content: + - 突然之間,你感覺到一股強大的力量從你的身體中湧出,你的身體開始發光,你的手中出現了一把魔法之劍。 + - type: dialogue + speaker: Unix Spirit + content: + - 恭喜您,我的指令官,您已成功解除了封印。 + - 請跟著在接下了關卡,一步步取回 Linux 之根吧! next: END diff --git a/server/quests/helloworld/home/forgotten_scroll.txt b/server/quests/helloworld/home/forgotten_scroll.txt new file mode 100644 index 0000000..17010b0 --- /dev/null +++ b/server/quests/helloworld/home/forgotten_scroll.txt @@ -0,0 +1,7 @@ +Once a mighty kingdom, now in darkness, +The power of Unix lies dormant, waiting. +To reclaim the root, the forgotten language, +Must be learned, commands of old rekindled. +Speak the words of power and awaken the might of Unix: + +Unix, rise and guide me! diff --git a/server/src/game/commandHandler.js b/server/src/game/commandHandler.js index c3587a3..4dd0df5 100644 --- a/server/src/game/commandHandler.js +++ b/server/src/game/commandHandler.js @@ -31,6 +31,8 @@ const commandHandler = async (session, commandInput) => { return { responses: stage.responses, + hints: stage.hints, + end: stage.next === 'END', } } diff --git a/server/src/models/quest.js b/server/src/models/quest.js index 2b46935..ea592f1 100644 --- a/server/src/models/quest.js +++ b/server/src/models/quest.js @@ -1,5 +1,15 @@ import { model, Schema } from 'mongoose' +const responseSchema = new Schema({ + type: { + type: String, + required: true, + }, + content: [String], + speaker: String, + color: String, +}) + const stageSchema = new Schema({ id: { type: String, @@ -20,7 +30,7 @@ const stageSchema = new Schema({ }, }, responses: { - type: [String], + type: [responseSchema], required: true, }, hints: { diff --git a/server/swagger.yaml b/server/swagger.yaml index 5532b60..5b671bb 100644 --- a/server/swagger.yaml +++ b/server/swagger.yaml @@ -453,10 +453,38 @@ components: type: object properties: responses: + type: array + items: + $ref: '#/components/schemas/CommandResponseItem' + hints: + type: array + items: + type: string + example: '`ls -al` to list detail information of all files.' + end: + type: boolean + description: Whether the session has ended + + CommandResponseItem: + type: object + required: + - type + - content + properties: + type: + type: string + enum: [narrative, dialogue] + content: type: array items: type: string example: 'You have completed the quest!' + color: + type: string + enum: [red, green, blue, yellow, purple, cyan, white, gray, black] + speaker: + type: string + example: 'Unix Spirit' securitySchemes: BearerAuth: