-
-
-
-
{{ current + 1 }}
-
/
-
- {{ sessionStore.session.hints.length }}
-
-
-
-
+
+
+
+
+
+
+ 提示
+
+
+
+
+
+
{{ current + 1 }}
+
/
+
+ {{ sessionStore.session.hints.length }}
+
+
+
+
+
+
+
-
diff --git a/app/src/config.js b/app/src/config.js
index 2c6d374..73879c5 100644
--- a/app/src/config.js
+++ b/app/src/config.js
@@ -1,7 +1,4 @@
export const surveyUrl = '/api/v1/survey'
export const bugReportUrl = '/api/v1/bug-report'
-
-export default {
- surveyUrl,
- bugReportUrl,
-}
+export const facebookUrl = 'https://www.facebook.com/LinuxOdyssey'
+export const githubUrl = 'https://github.com/linux-odyssey/linux-odyssey'
diff --git a/app/src/img/icon_settings.svg b/app/src/img/icon_settings.svg
new file mode 100644
index 0000000..15c61e6
--- /dev/null
+++ b/app/src/img/icon_settings.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/img/icon_shuttle.svg b/app/src/img/icon_shuttle.svg
new file mode 100644
index 0000000..878edb9
--- /dev/null
+++ b/app/src/img/icon_shuttle.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/img/icon_sword.svg b/app/src/img/icon_sword.svg
new file mode 100644
index 0000000..22eda38
--- /dev/null
+++ b/app/src/img/icon_sword.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/library.js b/app/src/library.js
index 1142033..7f3c00c 100644
--- a/app/src/library.js
+++ b/app/src/library.js
@@ -18,13 +18,18 @@ import {
faMap,
faTrophy,
faFileInvoice,
+ faEnvelope,
} from '@fortawesome/free-solid-svg-icons'
import {
faLightbulb,
faCircleQuestion,
faCircleRight,
} from '@fortawesome/free-regular-svg-icons'
-import { faGithub, faGoogle } from '@fortawesome/free-brands-svg-icons'
+import {
+ faFacebook,
+ faGithub,
+ faGoogle,
+} from '@fortawesome/free-brands-svg-icons'
/* add icons to the library */
library.add(
@@ -47,5 +52,7 @@ library.add(
faCircleRight,
faGithub,
faGoogle,
- faFileInvoice
+ faFileInvoice,
+ faFacebook,
+ faEnvelope
)
diff --git a/app/src/router.js b/app/src/router.js
index 1d3b420..d6fd78d 100644
--- a/app/src/router.js
+++ b/app/src/router.js
@@ -5,15 +5,14 @@ import RegisterPage from './views/RegisterPage.vue'
import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
import QuestMapPage from './views/QuestMapPage.vue'
import LeaderboardPage from './views/LeaderboardPage.vue'
+import LandingPage from './views/LandingPage.vue'
import { isLoggedIn } from './utils/auth'
const routes = [
{
path: '/',
- redirect: {
- name: 'register',
- },
- meta: { requiresGuest: true },
+ name: 'landing-page',
+ component: LandingPage,
},
{
path: '/login',
diff --git a/app/src/store/session.js b/app/src/store/session.js
index 5464bd0..9ab8295 100644
--- a/app/src/store/session.js
+++ b/app/src/store/session.js
@@ -20,6 +20,7 @@ function newSession() {
pwd: '',
hints: [],
tasks: [],
+ responses: [],
}
}
@@ -41,6 +42,7 @@ function setQuest(questId) {
async function setSession(session) {
try {
+ console.log('Setting session...', session)
store.session = session
store.session.graph = new FileGraph(session.graph)
term.reset()
@@ -85,16 +87,12 @@ function updateGraph(event) {
}
}
-function updateHints(hints) {
- store.session.hints.push(hints)
-}
-
-function updateStatus(status) {
- store.session.status = status
-}
-
-function setTasks(tasks) {
- store.session.tasks = tasks
+function newResponse(response) {
+ console.log('newResponse', response, 'session', store.session)
+ store.session.responses.push(response.responses)
+ store.session.hints.push(response.hints)
+ store.session.tasks = response.tasks
+ store.session.status = response.status
}
export function useTerminal() {
@@ -135,14 +133,8 @@ function setup() {
socket.on('graph', (event) => {
updateGraph(event)
})
- socket.on('hints', (event) => {
- updateHints(event)
- })
- socket.on('tasks', (tasks) => {
- setTasks(tasks)
- })
- socket.on('status', (event) => {
- updateStatus(event)
+ socket.on('response', (response) => {
+ newResponse(response)
})
}
diff --git a/app/src/views/GamePage.vue b/app/src/views/GamePage.vue
index a11e1ad..74db5ab 100644
--- a/app/src/views/GamePage.vue
+++ b/app/src/views/GamePage.vue
@@ -61,7 +61,7 @@ onMounted(async () => {
-
+
@@ -71,17 +71,17 @@ onMounted(async () => {
-->
-
-
-
+
+
-
-
+
diff --git a/app/src/views/LandingPage.vue b/app/src/views/LandingPage.vue
new file mode 100644
index 0000000..8db69bb
--- /dev/null
+++ b/app/src/views/LandingPage.vue
@@ -0,0 +1,299 @@
+
+
+
+
diff --git a/packages/models/src/session.ts b/packages/models/src/session.ts
index fdc749f..49185fd 100644
--- a/packages/models/src/session.ts
+++ b/packages/models/src/session.ts
@@ -1,5 +1,7 @@
import { model, Schema, Types } from 'mongoose'
+import { IResponse, responseSchema } from './response.js'
+
export interface INode {
path: string
type: string
@@ -46,13 +48,14 @@ export interface ISession {
user: Types.ObjectId
quest: string
containerId: string | null
- status: string
+ status: 'active' | 'finished' | 'inactive'
createdAt: Date
updatedAt: Date
finishedAt?: Date
lastActivityAt: Date
tasks: ITask[]
hints: string[][]
+ responses: IResponse[][]
graph: INode
}
@@ -85,6 +88,11 @@ export const Session = model(
},
tasks: [taskSchema],
hints: [[String]],
+ responses: {
+ type: [[responseSchema]],
+ required: true,
+ default: [],
+ },
graph: {
type: nodeSchema,
default: {
diff --git a/server/src/api/controllers/authController.ts b/server/src/api/controllers/authController.ts
index a86f0fb..258ba4f 100644
--- a/server/src/api/controllers/authController.ts
+++ b/server/src/api/controllers/authController.ts
@@ -67,7 +67,7 @@ export function socialLoginHandler(req: Request, res: Response) {
res.redirect('/choose-username')
return
}
- res.redirect('/')
+ res.redirect('/map')
}
export const registerFromSession = asyncHandler(
diff --git a/server/src/api/controllers/commandController.ts b/server/src/api/controllers/commandController.ts
index 5e4f5f2..4306601 100644
--- a/server/src/api/controllers/commandController.ts
+++ b/server/src/api/controllers/commandController.ts
@@ -6,8 +6,6 @@ import { pushToSession } from '../socket.js'
import { finishSession } from '../../models/sessionManager.js'
import { asyncHandler } from '../../middleware/error.js'
-const commandCompleteCallbacks = new Map()
-
export const newCommand = asyncHandler(async (req: Request, res: Response) => {
const { command, pwd, output, error, params } = matchedData(req)
@@ -51,42 +49,14 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => {
c.stage = response.stage
await c.save()
- if (response.stage === 'END') {
+ session.responses.push(response.responses)
+
+ if ((session.status as string) === 'finished') {
await finishSession(session)
}
- commandCompleteCallbacks.set(session.id, response.callback)
- pushToSession(session.id, 'status', 'pending')
- res.status(201).json(response)
- } else {
- res.status(200).end()
+ pushToSession(session.id, 'response', response)
}
+ res.status(200).end()
await session.save()
})
-
-export const completedCommand = asyncHandler(
- async (req: Request, res: Response) => {
- const { sessionId } = req.user as any
-
- const session = await Session.findById(sessionId)
-
- if (!session) {
- res.status(404).json({ message: 'session not found' })
- return
- }
-
- if (session.status === 'inactive') {
- res.status(400).json({ message: 'session is not active' })
- return
- }
-
- const callback = commandCompleteCallbacks.get(session.id)
- if (!callback) {
- res.status(200).json({ message: 'no callback found' })
- return
- }
- callback()
- commandCompleteCallbacks.delete(session.id)
- res.json({ message: 'ok' })
- }
-)
diff --git a/server/src/api/controllers/sessionController.ts b/server/src/api/controllers/sessionController.ts
index 2fb6f00..6c4f348 100644
--- a/server/src/api/controllers/sessionController.ts
+++ b/server/src/api/controllers/sessionController.ts
@@ -25,6 +25,7 @@ function sessionDetail(session: ISession) {
...sessionSummary(session),
hints: session.hints,
tasks: session.tasks,
+ responses: session.responses,
graph: session.graph,
}
}
@@ -55,6 +56,7 @@ export const createSession = asyncHandler(
export const getSessionById = asyncHandler(
async (req: Request, res: Response) => {
const { sessionId } = matchedData(req)
+ console.log(sessionId)
const session = await Session.findOne({
_id: sessionId,
user: (req.user as Express.ExistingUser).id,
diff --git a/server/src/api/routes/commandRouter.ts b/server/src/api/routes/commandRouter.ts
index ef22842..b255d21 100644
--- a/server/src/api/routes/commandRouter.ts
+++ b/server/src/api/routes/commandRouter.ts
@@ -5,6 +5,5 @@ import { newCommandValidator } from '../validators/commandValidator.js'
const router = Router()
router.post('/', newCommandValidator, commandController.newCommand)
-router.post('/completed', commandController.completedCommand)
export default router
diff --git a/server/src/api/routes/sessionRouter.ts b/server/src/api/routes/sessionRouter.ts
index ddc7cdb..52e4e0c 100644
--- a/server/src/api/routes/sessionRouter.ts
+++ b/server/src/api/routes/sessionRouter.ts
@@ -22,6 +22,10 @@ router.post(
activeSessionValidator,
sessionController.getOrCreateSession
)
-router.get('/:id', sessionDetailValidator, sessionController.getSessionById)
+router.get(
+ '/:sessionId',
+ sessionDetailValidator,
+ sessionController.getSessionById
+)
export default router
diff --git a/server/src/api/validators/sessionValidator.ts b/server/src/api/validators/sessionValidator.ts
index ddb3c84..558b5b6 100644
--- a/server/src/api/validators/sessionValidator.ts
+++ b/server/src/api/validators/sessionValidator.ts
@@ -9,6 +9,6 @@ export const checkSessionId = () =>
check('sessionId').notEmpty().isString().isMongoId()
export const sessionListValidator = [checkQuestId(), checkSessionStatus()]
-export const sessionDetailValidator = [checkSessionId(), noError]
+export const sessionDetailValidator = [checkSessionId()]
export const createSessionValidator = [checkQuestId(), noError]
export const activeSessionValidator = [checkQuestId(), noError]
diff --git a/server/src/game/sessionHandler.ts b/server/src/game/sessionHandler.ts
index 6808f5c..c061d57 100644
--- a/server/src/game/sessionHandler.ts
+++ b/server/src/game/sessionHandler.ts
@@ -8,12 +8,12 @@ import type {
IStage,
ITask,
} from '@linux-odyssey/models'
-import { pushToSession } from '../api/socket.js'
export interface ExecuteResult {
responses: IResponse[]
hints: string[]
- callback: () => void
+ tasks?: ITask[]
+ status: string
stage?: string
end?: boolean
}
@@ -79,15 +79,17 @@ export default class SessionHandler {
this.addNewTasks()
+ this.session.responses.push(stage.responses)
+
+ if (stage.id === 'END') {
+ this.session.status = 'finished'
+ }
+
return {
responses: stage.responses,
hints: stage.hints,
- callback: () => {
- pushToSession(this.session.id, 'tasks', this.session.tasks)
- if (stage.hints && stage.hints.length > 0)
- pushToSession(this.session.id, 'hints', stage.hints)
- pushToSession(this.session.id, 'status', this.session.status)
- },
+ tasks: this.session.tasks,
+ status: this.session.status,
}
}
@@ -98,11 +100,7 @@ export default class SessionHandler {
return {
responses,
hints,
- callback: () => {
- if (hints && hints.length > 0)
- pushToSession(this.session.id, 'hints', hints)
- pushToSession(this.session.id, 'status', this.session.status)
- },
+ status: this.session.status,
}
}
}
diff --git a/tests/cypress/e2e/UItests.cy.ts b/tests/cypress/e2e/UItests.cy.ts
index 1ea8646..031e104 100644
--- a/tests/cypress/e2e/UItests.cy.ts
+++ b/tests/cypress/e2e/UItests.cy.ts
@@ -75,7 +75,7 @@ describe('example helloworld app', () => {
describe('Game Start Page UI', () => {
const title = '終端機的基本知識'
beforeEach(() => {
- cy.PrepareForGame()
+ cy.PrepareForGame('get-started')
})
it('Check Header', () => {
cy.get('#GameTitle').should('contain', 'Linux Odyssey').and('be.visible')
@@ -124,7 +124,7 @@ describe('example helloworld app', () => {
})
describe('Game Play', () => {
beforeEach(() => {
- cy.PrepareForGame()
+ cy.PrepareForGame('get-started')
})
it('Typing in Terminal', () => {
cy.typeInCommand('12345{enter}')
@@ -136,7 +136,7 @@ describe('example helloworld app', () => {
cy.checkTaskInit()
cy.InitTerminal()
cy.typeInCommand('echo start{enter}')
- cy.checkPending()
+ // cy.checkPending()
cy.waitUntilActive()
cy.get('#Lbutton').should('be.visible').and('be.disabled')
cy.get('#Rbutton').should('be.visible').and('be.disabled')
@@ -144,7 +144,7 @@ describe('example helloworld app', () => {
cy.getQuestInfo('✓ 輸入 echo start 來開始教程').should('be.visible')
cy.getQuestInfo('➤ 在終端機輸入 echo hello').should('be.visible')
cy.typeInCommand('echo hello{enter}')
- cy.checkPending()
+ // cy.checkPending()
cy.waitUntilActive()
cy.get('#Lbutton').should('be.visible').and('be.enabled')
cy.get('#Rbutton').should('be.visible').and('be.disabled')
diff --git a/tests/cypress/e2e/completeQuests.cy.ts b/tests/cypress/e2e/completeQuests.cy.ts
index 57ba8e2..dbd9de8 100644
--- a/tests/cypress/e2e/completeQuests.cy.ts
+++ b/tests/cypress/e2e/completeQuests.cy.ts
@@ -1,7 +1,4 @@
describe('Complete quests with only commands', () => {
- beforeEach(() => {
- cy.PrepareForGame()
- })
it('Complete quest : get-started', () => {
cy.CompleteStageWithCommands('get-started')
})
diff --git a/tests/cypress/support/commands.ts b/tests/cypress/support/commands.ts
index b77cd88..23d6484 100644
--- a/tests/cypress/support/commands.ts
+++ b/tests/cypress/support/commands.ts
@@ -38,7 +38,7 @@ Cypress.Commands.add('LoginWithPassword', (username, password) => {
cy.get('#password').type(password)
cy.get('#LogInOrSignUp').click()
})
-Cypress.Commands.add('PrepareForGame', () => {
+Cypress.Commands.add('PrepareForGame', (questId) => {
cy.visit('/')
// make sure the login page is loaded
cy.url().should('satisfy', (elements: string) => {
@@ -51,8 +51,9 @@ Cypress.Commands.add('PrepareForGame', () => {
)
// make sure the map page is loaded
cy.url().should('include', '/map')
- cy.visit('/game/get-started')
- cy.url().should('include', '/game/get-started')
+ const url = `/game/${questId}`
+ cy.visit(url)
+ cy.url().should('include', url)
// check task first so the terminal has better possibility loaded
cy.checkTaskInit()
cy.get('.xterm-screen', { timeout: 10000 })
@@ -114,8 +115,7 @@ Cypress.Commands.add('checkPending', () => {
)
})
Cypress.Commands.add('CompleteStageWithCommands', (stagename) => {
- cy.visit(`/game/${stagename}`)
- cy.url().should('include', `/game/${stagename}`, { timeout: 50000 })
+ cy.PrepareForGame(stagename)
cy.log(`Completing stage :${stagename}`)
cy.checkTaskInit()
cy.CheckTextElement('#reset', '重來', 'Reset').click()
@@ -123,19 +123,22 @@ Cypress.Commands.add('CompleteStageWithCommands', (stagename) => {
cy.InitTerminal()
cy.readFile(`../quests/${stagename}/answer.sh`, 'utf-8').then((answers) => {
const answerarr = answers.split('\n')
- for (const element of answerarr) {
+ answerarr.forEach((element, index) => {
cy.typeInCommand(`${element}{enter}`)
- if (answerarr.indexOf(element) + 1 !== answerarr.length) {
- cy.checkPending()
+ if (index < answerarr.length - 1) {
cy.waitUntilActive()
+ } else {
+ cy.waitUntilActive(true).then(() => {
+ cy.CheckTextElement(
+ '#QuestCompleted',
+ '關卡完成!',
+ 'Quest Completed!'
+ )
+ cy.get('div[class="modal"]').find('p').should('be.visible')
+ cy.get('#BacktoMap').should('be.visible').and('contain', '回到地圖')
+ })
}
- if (answerarr.indexOf(element) + 1 === answerarr.length) {
- cy.waitUntilActive(true)
- cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!')
- cy.get('div[class="modal"]').find('p').should('be.visible')
- cy.get('#BacktoMap').should('be.visible').and('contain', '回到地圖')
- }
- }
+ })
})
})
Cypress.Commands.add('CheckTextElement', (id, chText, enText) => {