diff --git a/.eslintrc.js b/.eslintrc.js index 1d3cb3d..7410a96 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,7 +13,7 @@ module.exports = { ], parserOptions: { // parser: 'babel-eslint', - ecmaVersion: 2020, + ecmaVersion: 13, sourceType: 'module', }, rules: { diff --git a/app/Dockerfile b/app/Dockerfile index df72388..484da71 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -2,10 +2,10 @@ FROM node:20-alpine as base WORKDIR /app COPY package.json yarn.lock ./ -COPY app/package.json ./app/package.json COPY packages/file-graph/package.json ./packages/file-graph/package.json COPY packages/utils/package.json ./packages/utils/package.json COPY packages/constants/package.json ./packages/constants/package.json +COPY app/package.json ./app/package.json RUN yarn install --frozen-lockfile # Dev stage diff --git a/app/package.json b/app/package.json index c09dc4e..4fbe9d2 100644 --- a/app/package.json +++ b/app/package.json @@ -20,6 +20,7 @@ "@linux-odyssey/file-graph": "*", "@linux-odyssey/utils": "*", "axios": "^1.6.0", + "echarts": "^5.4.3", "socket.io-client": "^4.7.1", "validator": "^13.11.0", "vue": "^3.3.4", diff --git a/app/src/components/QuestMap.vue b/app/src/components/QuestMap.vue new file mode 100644 index 0000000..18ed7e6 --- /dev/null +++ b/app/src/components/QuestMap.vue @@ -0,0 +1,158 @@ + + + diff --git a/app/src/img/catelogbg.svg b/app/src/img/catelogbg.svg new file mode 100644 index 0000000..892031b --- /dev/null +++ b/app/src/img/catelogbg.svg @@ -0,0 +1,1130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/img/rect.svg b/app/src/img/rect.svg new file mode 100644 index 0000000..15481fd --- /dev/null +++ b/app/src/img/rect.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/img/svg.js b/app/src/img/svg.js new file mode 100644 index 0000000..13f5e2c --- /dev/null +++ b/app/src/img/svg.js @@ -0,0 +1,2 @@ +export const NodeImage = + 'path://M0.640015 25.0339C0.640015 11.2268 11.8329 0.0339355 25.64 0.0339355H246.36C260.167 0.0339355 271.36 11.2268 271.36 25.0339V113.14C271.36 126.948 260.167 138.14 246.36 138.14H25.64C11.8329 138.14 0.640015 126.948 0.640015 113.14V25.0339Z' diff --git a/app/src/router.js b/app/src/router.js index 9df8a54..060af3f 100644 --- a/app/src/router.js +++ b/app/src/router.js @@ -3,6 +3,7 @@ import GamePage from './views/GamePage.vue' import LoginPage from './views/LoginPage.vue' import RegisterPage from './views/RegisterPage.vue' import ChooseUsernamePage from './views/ChooseUsernamePage.vue' +import QuestMapPage from './views/QuestMapPage.vue' import { isLoggedIn } from './utils/auth' const routes = [ @@ -40,6 +41,12 @@ const routes = [ component: ChooseUsernamePage, meta: { requiresGuest: true }, }, + { + path: '/map', + name: 'map', + component: QuestMapPage, + meta: { requiresAuth: true }, + }, ] const router = createRouter({ diff --git a/app/src/views/QuestMapPage.vue b/app/src/views/QuestMapPage.vue new file mode 100644 index 0000000..56a904a --- /dev/null +++ b/app/src/views/QuestMapPage.vue @@ -0,0 +1,19 @@ + + + diff --git a/docker-compose.yml b/docker-compose.yml index 3aeaf0b..38e399f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -78,15 +78,6 @@ services: volumes: - ./data:/data/db - swagger: - image: swaggerapi/swagger-editor:latest - ports: - - 8080:8080 - volumes: - - ./server/swagger.yaml:/tmp/swagger.yaml - environment: - - SWAGGER_FILE=/tmp/swagger.yaml - base: image: linuxodyssey/quest-base build: diff --git a/packages/utils/dag.js b/packages/utils/dag.js new file mode 100644 index 0000000..6a3a911 --- /dev/null +++ b/packages/utils/dag.js @@ -0,0 +1,73 @@ +export default class DAG { + #nodes = new Map() + + #edges = new Set() + + #layers = [] + + constructor(nodes) { + this.#nodes = new Map(nodes.map((node) => [node._id, node])) + this.#edges = new Set() + this.#nodes.forEach((_, id) => { + this.#setLayer(id) + this.#addEdges(id) + }) + } + + getNode(id) { + return this.#nodes.get(id) + } + + getNodes() { + return Array.from(this.#nodes.values()) + } + + getEdges() { + return this.#edges + } + + getEdgesArray() { + return Array.from(this.#edges.values()) + } + + getLayers() { + return this.#layers + } + + getLayer(id) { + const node = this.getNode(id) + if (!node) { + throw new Error(`Node ${id} not found`) + } + return this.#layers[node.layer - 1] + } + + #setLayer(id) { + const node = this.getNode(id) + if (!node) { + throw new Error(`Node ${id} not found`) + } + if (node.layer !== undefined) { + return node.layer + } + const previousLayer = node.requirements.map((rId) => this.#setLayer(rId)) + node.layer = Math.max(...previousLayer, 0) + 1 + if (this.#layers.length < node.layer) { + this.#layers.push(1) + } else { + this.#layers[node.layer - 1] += 1 + } + node.index = this.#layers[node.layer - 1] + return node.layer + } + + #addEdges(id) { + const node = this.getNode(id) + if (!node) { + throw new Error(`Node ${id} not found`) + } + node.requirements.forEach((rId) => { + this.#edges.add([rId, id]) + }) + } +} diff --git a/packages/utils/dag.test.js b/packages/utils/dag.test.js new file mode 100644 index 0000000..e296782 --- /dev/null +++ b/packages/utils/dag.test.js @@ -0,0 +1,165 @@ +import DAG from './dag.js' + +describe('dag layers', () => { + it('two layer', () => { + const data = [ + { + _id: 'helloworld', + requirements: [], + }, + { + _id: 'spell', + requirements: ['helloworld'], + }, + ] + const dag = new DAG(data) + expect(dag.getNode('helloworld').layer).toBe(1) + expect(dag.getNode('spell').layer).toBe(2) + }) + it('branch', () => { + const data = [ + { + _id: 'helloworld', + requirements: [], + }, + { + _id: 'spell', + requirements: ['helloworld'], + }, + { + _id: 'discover', + requirements: ['helloworld'], + }, + ] + const dag = new DAG(data) + expect(dag.getNode('helloworld').layer).toBe(1) + expect(dag.getNode('spell').layer).toBe(2) + expect(dag.getNode('discover').layer).toBe(2) + }) + it('merge', () => { + const data = [ + { + _id: 'A', + requirements: [], + }, + { + _id: 'B', + requirements: [], + }, + { + _id: 'C', + requirements: ['A', 'B'], + }, + ] + const dag = new DAG(data) + expect(dag.getNode('A').layer).toBe(1) + expect(dag.getNode('B').layer).toBe(1) + expect(dag.getNode('C').layer).toBe(2) + }) + it('skip', () => { + const data = [ + { + _id: 'A', + requirements: [], + }, + { + _id: 'B', + requirements: [], + }, + { + _id: 'C', + requirements: ['A'], + }, + { + _id: 'D', + requirements: ['B', 'C'], + }, + ] + const dag = new DAG(data) + expect(dag.getNode('A').layer).toBe(1) + expect(dag.getNode('B').layer).toBe(1) + expect(dag.getNode('C').layer).toBe(2) + expect(dag.getNode('D').layer).toBe(3) + }) +}) + +describe('dag edges', () => { + it('one edge', () => { + const data = [ + { + _id: 'helloworld', + requirements: [], + }, + { + _id: 'spell', + requirements: ['helloworld'], + }, + ] + + const dag = new DAG(data) + expect(dag.getEdges()).toEqual(new Set([['helloworld', 'spell']])) + }) + + it('branch edges', () => { + const data = [ + { + _id: 'helloworld', + requirements: [], + }, + { + _id: 'spell', + requirements: ['helloworld'], + }, + { + _id: 'discover', + requirements: ['helloworld'], + }, + ] + + const dag = new DAG(data) + expect(dag.getEdges()).toEqual( + new Set([ + ['helloworld', 'discover'], + ['helloworld', 'spell'], + ]) + ) + }) +}) + +describe('dag layers', () => { + it('two layer', () => { + const data = [ + { + _id: 'helloworld', + requirements: [], + }, + { + _id: 'spell', + requirements: ['helloworld'], + }, + ] + + const dag = new DAG(data) + expect(dag.getLayers()).toEqual([1, 1]) + }) + + it('branch layers', () => { + const data = [ + { + _id: 'helloworld', + requirements: [], + }, + { + _id: 'spell', + requirements: ['helloworld'], + }, + { + _id: 'discover', + requirements: ['helloworld'], + }, + ] + + const dag = new DAG(data) + expect(dag.getLayers()).toEqual([1, 2]) + }) +}) diff --git a/packages/utils/index.js b/packages/utils/index.js index 8d79493..83029e5 100644 --- a/packages/utils/index.js +++ b/packages/utils/index.js @@ -1,2 +1,3 @@ export { isValidEmail, isValidUsername } from './auth.js' export { buildFileCheckCmd } from './commands.js' +export { default as DAG } from './dag.js' diff --git a/quests/create/game.yml b/quests/create/game.yml index f4a7260..5d314d9 100644 --- a/quests/create/game.yml +++ b/quests/create/game.yml @@ -1,3 +1,3 @@ -title: Read the File -instruction: | - 閱讀檔案 + +title: 建立檔案與資料夾 +instruction: | \ No newline at end of file diff --git a/quests/meta.yml b/quests/meta.yml index b3635ef..3a33b62 100644 --- a/quests/meta.yml +++ b/quests/meta.yml @@ -3,8 +3,8 @@ quests: - cast - discover - read - - create - transport + - create - copy - move - remove diff --git a/server/src/api/controllers/sessionController.js b/server/src/api/controllers/sessionController.js index 05188fb..f21b6e0 100644 --- a/server/src/api/controllers/sessionController.js +++ b/server/src/api/controllers/sessionController.js @@ -6,6 +6,7 @@ import { isQuestUnlocked, } from '../../models/sessionManager.js' import { asyncHandler } from '../../middleware/error.js' +import config from '../../config.js' function sessionSummary(session) { return { @@ -65,7 +66,7 @@ export const getSessionById = asyncHandler(async (req, res) => { export const getOrCreateSession = asyncHandler(async (req, res) => { const { questId } = matchedData(req) - if (await isQuestUnlocked(req.user, questId)) { + if ((await isQuestUnlocked(req.user, questId)) || config.testing.enable) { const session = await getOrCreateActiveSession(req.user, questId) res.json(sessionDetail(session)) } else { diff --git a/server/src/models/sessionManager.js b/server/src/models/sessionManager.js index 8a9bbb9..715d67f 100644 --- a/server/src/models/sessionManager.js +++ b/server/src/models/sessionManager.js @@ -110,6 +110,9 @@ export async function isQuestUnlocked(user, questId) { if (!quest) { throw new Error(`Quest ${questId} not found`) } + if (!quest.requirements) { + return true + } return quest.requirements.every((requiredQuestId) => { const progress = userProfile.progress.get(requiredQuestId) return progress && progress.completed diff --git a/server/src/utils/quest.js b/server/src/utils/quest.js index f7e4b28..c2aaf79 100644 --- a/server/src/utils/quest.js +++ b/server/src/utils/quest.js @@ -22,7 +22,8 @@ async function mapQuests(callback) { ) } -export function loadAndUpdateQuests() { +export async function loadAndUpdateQuests() { + await Quest.deleteMany({}) return mapQuests(async (id, questPath) => { const files = await fs.readdir(questPath) if (!files.includes('game.yml')) { @@ -32,23 +33,17 @@ export function loadAndUpdateQuests() { try { const body = await fs.readFile(path.join(questPath, 'game.yml'), 'utf-8') - const quest = yaml.parse(body, { + const questData = yaml.parse(body, { merge: true, }) const image = files.includes('Dockerfile') ? id : 'base' - - return await Quest.findByIdAndUpdate( - id, - { - _id: id, - image, - ...quest, - }, - { - upsert: true, - } - ) + const quest = new Quest({ + _id: id, + image, + ...questData, + }) + await quest.save() } catch (error) { console.error(`Error parsing quest ${id}:`, error.message) throw error diff --git a/yarn.lock b/yarn.lock index bd4e3ec..9f3d531 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2367,6 +2367,14 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" +echarts@^5.4.3: + version "5.4.3" + resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.4.3.tgz#f5522ef24419164903eedcfd2b506c6fc91fb20c" + integrity sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA== + dependencies: + tslib "2.3.0" + zrender "5.4.4" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -6238,6 +6246,11 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== + tslib@^2.1.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" @@ -6691,3 +6704,10 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zrender@5.4.4: + version "5.4.4" + resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.4.4.tgz#8854f1d95ecc82cf8912f5a11f86657cb8c9e261" + integrity sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw== + dependencies: + tslib "2.3.0"