mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
feat: support changing quest language
This commit is contained in:
1 parent
9b8d7109cf
commit
66b0b7ba68
18 files changed
+281
-77
No files matched your search
@@ -5,8 +5,9 @@ import { useToast } from 'vue-toastification'
|
||||
import { ref, defineProps, onMounted } from 'vue'
|
||||
import MarkdownText from './MarkdownText.vue'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
questTitle: {
|
||||
@@ -52,8 +53,15 @@ const handleColour = () => {
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
const quest = await trpc.quests.getQuestDetail.query(questId)
|
||||
try {
|
||||
const quest = await trpc.quests.getQuestDetail.query({
|
||||
questId,
|
||||
locale: locale.value,
|
||||
})
|
||||
questInstruction.value = quest.instruction
|
||||
} catch (error) {
|
||||
toast.error('Failed to load quest detail:', error)
|
||||
}
|
||||
handleColour()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
||||
import { DAG } from '../../../packages/utils'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import useUserProfile from '../store/userProfile'
|
||||
import QuestIntro from './QuestIntro.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
const store = useUserProfile()
|
||||
const toast = useToast()
|
||||
|
||||
@@ -66,7 +67,8 @@ async function computeGraphData() {
|
||||
if (!store.progress) return
|
||||
|
||||
try {
|
||||
const quests = await trpc.quests.getQuests.query()
|
||||
const quests = await trpc.quests.getQuests.query({ locale: locale.value })
|
||||
|
||||
const dag = new DAG(quests)
|
||||
const nodesValues = dag.getNodes()
|
||||
|
||||
@@ -117,6 +119,11 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
watch(() => store.progress, computeGraphData, { deep: true })
|
||||
watch(locale, () => {
|
||||
opened.value = null
|
||||
// eslint-disable-next-line no-void
|
||||
void computeGraphData()
|
||||
})
|
||||
|
||||
const curvedPath = computed(() => {
|
||||
return (source: Node, target: Node) => {
|
||||
|
||||
@@ -4,15 +4,44 @@ import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useSession } from '../../store/session'
|
||||
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import type { QuestLocale } from '../../../../packages/constants'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const router = useRouter()
|
||||
const sessionStore = useSession()
|
||||
const reset = async () => {
|
||||
console.log('resetting...')
|
||||
await sessionStore.createSession()
|
||||
const selectedLocale = locale.value as QuestLocale
|
||||
const questId = sessionStore.session?.quest
|
||||
|
||||
if (!questId) return
|
||||
|
||||
try {
|
||||
const quests = await trpc.quests.getQuests.query({
|
||||
locale: selectedLocale,
|
||||
})
|
||||
|
||||
console.log({
|
||||
selectedLocale,
|
||||
questId,
|
||||
availableQuestIds: quests.map((quest) => quest.id),
|
||||
})
|
||||
|
||||
if (!quests.some((quest) => quest.id === questId)) {
|
||||
toast.warning(t('game.language_unavailable'))
|
||||
return
|
||||
}
|
||||
|
||||
await sessionStore.createSession(selectedLocale)
|
||||
} catch (error) {
|
||||
console.error('Failed to reset quest:', error)
|
||||
toast.error(t('game.reset_failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const continuePlay = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
|
||||
@@ -3,8 +3,9 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { computed } from 'vue'
|
||||
import { useSession } from '../../store/session'
|
||||
import { SUPPORTED_LOCALES } from '../../../../packages/constants'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
const sessionStore = useSession()
|
||||
@@ -13,7 +14,7 @@ const showCover = computed(() => sessionStore.session === null)
|
||||
|
||||
const startSession = async () => {
|
||||
try {
|
||||
await sessionStore.createSession()
|
||||
await sessionStore.createSession(locale.value)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
toast.error(err.message)
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { setLocale, type SupportedLocale } from '../../i18n'
|
||||
import { setLocale } from '../../i18n'
|
||||
import type { QuestLocale } from '../../../../packages/constants'
|
||||
import {
|
||||
SUPPORTED_LOCALES,
|
||||
LOCALE_NAME_MAP,
|
||||
} from '../../../../packages/constants'
|
||||
|
||||
const { locale } = useI18n({ useScope: 'global' })
|
||||
const { t, locale } = useI18n({ useScope: 'global' })
|
||||
|
||||
function changeLanguage(event: Event) {
|
||||
const selectedLocale = (event.target as HTMLSelectElement)
|
||||
.value as SupportedLocale
|
||||
.value as QuestLocale
|
||||
|
||||
setLocale(selectedLocale)
|
||||
}
|
||||
@@ -15,12 +20,14 @@ function changeLanguage(event: Event) {
|
||||
<template>
|
||||
<select
|
||||
:value="locale"
|
||||
:title="t('language.change')"
|
||||
class="bg-bg-secondary px-3 py-2 text-sm text-text shadow-md outline-none"
|
||||
aria-label="Language"
|
||||
@change="changeLanguage"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="zh-TW">繁體中文</option>
|
||||
<option v-for="locale in SUPPORTED_LOCALES" :key="locale" :value="locale">
|
||||
{{ LOCALE_NAME_MAP[locale] }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
@@ -1,16 +1,14 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import type { QuestLocale } from '../../../packages/constants'
|
||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
||||
import en from './locales/en.json'
|
||||
import zhTW from './locales/zh-TW.json'
|
||||
|
||||
export const supportedLocales = ['zh-TW', 'en'] as const
|
||||
|
||||
export type SupportedLocale = (typeof supportedLocales)[number]
|
||||
|
||||
function isSupportedLocale(locale: string): locale is SupportedLocale {
|
||||
return supportedLocales.includes(locale as SupportedLocale)
|
||||
function isSupportedLocale(locale: string): locale is QuestLocale {
|
||||
return SUPPORTED_LOCALES.includes(locale as QuestLocale)
|
||||
}
|
||||
|
||||
function getInitialLocale(): SupportedLocale {
|
||||
function getInitialLocale(): QuestLocale {
|
||||
const savedLocale = localStorage.getItem('locale')
|
||||
|
||||
if (savedLocale && isSupportedLocale(savedLocale)) {
|
||||
@@ -34,7 +32,7 @@ export const i18n = createI18n({
|
||||
},
|
||||
})
|
||||
|
||||
export function setLocale(locale: SupportedLocale) {
|
||||
export function setLocale(locale: QuestLocale) {
|
||||
i18n.global.locale.value = locale
|
||||
localStorage.setItem('locale', locale)
|
||||
document.documentElement.lang = locale
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"language": {
|
||||
"change": "Change language",
|
||||
"traditional_chinese": "繁體中文",
|
||||
"english": "English"
|
||||
"change": "Change language"
|
||||
},
|
||||
"landing_nav": {
|
||||
"intro": "Introduction",
|
||||
@@ -102,6 +100,9 @@
|
||||
"reset": "Reset",
|
||||
"continue": "Continue",
|
||||
"command_book": "Command Book",
|
||||
"cheatsheets": "Cheat Sheets"
|
||||
"cheatsheets": "Cheat Sheets",
|
||||
"reset_to_apply_language": "Reset quest to apply language changes",
|
||||
"reset_failed": "Reset failed",
|
||||
"language_unavailable": "This quest does not support the selected language, please choose another language"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"language": {
|
||||
"change": "切換語言",
|
||||
"traditional_chinese": "繁體中文",
|
||||
"english": "English"
|
||||
"change": "切換語言"
|
||||
},
|
||||
"landing_nav": {
|
||||
"intro": "前言",
|
||||
@@ -102,6 +100,9 @@
|
||||
"reset": "重置",
|
||||
"continue": "繼續",
|
||||
"command_book": "指令之書",
|
||||
"cheatsheets": "指令列表"
|
||||
"cheatsheets": "指令列表",
|
||||
"reset_to_apply_language": "重置關卡以套用語言變更",
|
||||
"reset_failed": "重置失敗",
|
||||
"language_unavailable": "此關卡不支援所選語言,請選擇其他語言"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { QuestLocale } from '../../../packages/constants'
|
||||
import {
|
||||
FileGraph,
|
||||
type FileGraphUpdateEvent,
|
||||
@@ -41,11 +42,16 @@ export const useSession = defineStore('session', {
|
||||
quest: null,
|
||||
}),
|
||||
actions: {
|
||||
async setQuest(questId: string) {
|
||||
this.quest = await trpc.quests.getQuestDetail.query(questId)
|
||||
async setQuest(questId: string, locale: QuestLocale) {
|
||||
this.quest = await trpc.quests.getQuestDetail.query({
|
||||
questId,
|
||||
locale,
|
||||
})
|
||||
this.questId = questId
|
||||
},
|
||||
async setSession(session: SessionDetail) {
|
||||
await this.setQuest(session.quest, session.locale)
|
||||
|
||||
this.session = {
|
||||
...session,
|
||||
graph: new FileGraph(session.graph),
|
||||
@@ -59,19 +65,18 @@ export const useSession = defineStore('session', {
|
||||
)
|
||||
term.focus()
|
||||
},
|
||||
async createSession() {
|
||||
async createSession(locale: QuestLocale) {
|
||||
const session = await trpc.session.createSession.mutate({
|
||||
questId: this.questId,
|
||||
locale,
|
||||
})
|
||||
await this.setSession(session)
|
||||
term.send('echo start\n')
|
||||
},
|
||||
async getActiveSession() {
|
||||
const session = await trpc.session.getActiveSession.query({
|
||||
questId: this.questId,
|
||||
})
|
||||
async getActiveSession(questId: string) {
|
||||
const session = await trpc.session.getActiveSession.query({ questId })
|
||||
if (session) {
|
||||
this.setSession(session)
|
||||
await this.setSession(session)
|
||||
}
|
||||
},
|
||||
newUpdate(update: SessionUpdate) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useSession } from '../store/session'
|
||||
import useUserProfile from '../store/userProfile'
|
||||
@@ -13,6 +15,8 @@ import CompleteModal from '../components/game/CompleteModal.vue'
|
||||
import StartButton from '../components/game/StartButton.vue'
|
||||
import { openQuestSurvey } from '../utils/formbricks'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const toast = useToast()
|
||||
const sessionStore = useSession()
|
||||
const userStore = useUserProfile()
|
||||
|
||||
@@ -27,6 +31,13 @@ watch(completed, (newValue, oldValue) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(locale, (newLocale) => {
|
||||
const { session } = sessionStore
|
||||
if (session && newLocale !== session.locale) {
|
||||
toast.info(t('game.reset_to_apply_language'))
|
||||
}
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
questId: {
|
||||
type: String,
|
||||
@@ -37,8 +48,12 @@ const props = defineProps({
|
||||
onMounted(async () => {
|
||||
sessionStore.reset()
|
||||
sessionStore.setup()
|
||||
await sessionStore.setQuest(props.questId)
|
||||
await sessionStore.getActiveSession()
|
||||
|
||||
await sessionStore.getActiveSession(props.questId)
|
||||
|
||||
if (!sessionStore.session) {
|
||||
await sessionStore.setQuest(props.questId, locale.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -16,3 +16,10 @@ export const getSessionsRequestSchema = z.object({
|
||||
})
|
||||
|
||||
export type GetSessionsRequest = z.infer<typeof getSessionsRequestSchema>
|
||||
|
||||
export const SUPPORTED_LOCALES = ['zh-TW', 'en'] as const
|
||||
export const LOCALE_NAME_MAP: Record<string, string> = {
|
||||
'zh-TW': '繁體中文',
|
||||
en: 'English',
|
||||
}
|
||||
export type QuestLocale = (typeof SUPPORTED_LOCALES)[number]
|
||||
@@ -1,4 +1,6 @@
|
||||
import { model, Schema, Types } from 'mongoose'
|
||||
import type { QuestLocale } from '../constants'
|
||||
import { SUPPORTED_LOCALES } from '../constants'
|
||||
|
||||
export interface INode {
|
||||
path: string
|
||||
@@ -27,6 +29,7 @@ export interface ISession {
|
||||
_id: Types.ObjectId
|
||||
user: Types.ObjectId
|
||||
quest: string
|
||||
locale: QuestLocale
|
||||
containerId: string | null
|
||||
containerName: string | null
|
||||
status: 'active' | 'finished' | 'inactive'
|
||||
@@ -52,6 +55,11 @@ export const Session = model<ISession>(
|
||||
ref: 'Quest',
|
||||
required: true,
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
enum: SUPPORTED_LOCALES,
|
||||
required: true,
|
||||
},
|
||||
containerId: String,
|
||||
containerName: String,
|
||||
status: {
|
||||
|
||||
@@ -54,7 +54,7 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => {
|
||||
|
||||
session.lastActivityAt = new Date()
|
||||
|
||||
const quest = await questManager.get(session.quest)
|
||||
const quest = await questManager.get(session.quest, session.locale)
|
||||
if (!quest) {
|
||||
res.status(404).json({ message: 'quest not found' })
|
||||
return
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from 'fs/promises'
|
||||
import Docker from 'dockerode'
|
||||
import type { QuestLocale } from '../../../packages/constants'
|
||||
import config, { getQuestImage } from '../config.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
@@ -36,19 +37,20 @@ const newContainerOptions = (
|
||||
export async function createContainer(
|
||||
name: string,
|
||||
questId: string,
|
||||
locale: QuestLocale,
|
||||
imageId: string
|
||||
): Promise<Docker.Container> {
|
||||
const binds: string[] = []
|
||||
if (await questHomeExists(questId)) {
|
||||
if (await questHomeExists(questId, locale)) {
|
||||
logger.info('Mounting quest home', questId)
|
||||
binds.push(
|
||||
`${config.docker.hostProjectRoot}/quests/${questId}/home:/etc/skel:ro`
|
||||
`${config.docker.hostProjectRoot}/quests/${questId}/${locale}/home:/etc/skel:ro`
|
||||
)
|
||||
}
|
||||
if (config.docker.mountQuest && imageId !== 'base') {
|
||||
logger.info('Mounting quest directory', questId)
|
||||
binds.push(
|
||||
`${config.docker.hostProjectRoot}/quests/${questId}/home:/home/commander`
|
||||
`${config.docker.hostProjectRoot}/quests/${questId}/${locale}/home:/home/commander`
|
||||
)
|
||||
}
|
||||
if (config.docker.mountCLI) {
|
||||
@@ -75,9 +77,11 @@ async function createNetworkIfNotExists(network: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function questHomeExists(imageId: string) {
|
||||
async function questHomeExists(imageId: string, locale: QuestLocale) {
|
||||
try {
|
||||
const stat = await fs.stat(`${config.projectRoot}/quests/${imageId}/home`)
|
||||
const stat = await fs.stat(
|
||||
`${config.projectRoot}/quests/${imageId}/${locale}/home`
|
||||
)
|
||||
return stat.isDirectory()
|
||||
} catch {
|
||||
return false
|
||||
|
||||
+100
-15
@@ -3,6 +3,8 @@ import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import yaml from 'yaml'
|
||||
|
||||
import type { QuestLocale } from '../../../packages/constants'
|
||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
||||
import {
|
||||
questSchema,
|
||||
globalExceptionSchema,
|
||||
@@ -36,16 +38,22 @@ class QuestValidationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function questImageId(id: string, locale: QuestLocale) {
|
||||
return `${id}-${locale.toLowerCase()}`
|
||||
}
|
||||
|
||||
class QuestManager {
|
||||
private quests = new Map<string, IQuest>()
|
||||
private quests = new Map<string, Map<QuestLocale, IQuest>>()
|
||||
private questDirectory = path.join(config.projectRoot, 'quests')
|
||||
|
||||
get(id: string) {
|
||||
return this.quests.get(id)
|
||||
get(id: string, locale: QuestLocale) {
|
||||
return this.quests.get(id)?.get(locale)
|
||||
}
|
||||
|
||||
getAll() {
|
||||
getAll(locale: QuestLocale) {
|
||||
return Array.from(this.quests.values())
|
||||
.map((versions) => versions.get(locale))
|
||||
.filter((quest): quest is IQuest => quest !== undefined)
|
||||
}
|
||||
|
||||
private async mapQuests(callback: QuestCallback) {
|
||||
@@ -83,15 +91,44 @@ class QuestManager {
|
||||
|
||||
async loadAndUpdateQuests() {
|
||||
const exceptions = await this.loadGlobalExceptions()
|
||||
return this.mapQuests(async (id, questPath) => {
|
||||
const files = await fs.readdir(questPath)
|
||||
if (!files.includes('game.yml')) {
|
||||
throw new QuestValidationError(`Missing game.yml`, id, null)
|
||||
const loaded = new Map<string, Map<QuestLocale, IQuest>>()
|
||||
|
||||
await this.mapQuests(async (id, questPath) => {
|
||||
const entries = await fs.readdir(questPath, { withFileTypes: true })
|
||||
const versions = new Map<QuestLocale, IQuest>()
|
||||
|
||||
for (const locale of SUPPORTED_LOCALES) {
|
||||
const localeEntry = entries.find((entry) => entry.name === locale)
|
||||
|
||||
if (!localeEntry) {
|
||||
logger.warn(`Missing locale "${locale}" for quest "${id}"`)
|
||||
// eslint-disable-next-line no-continue
|
||||
continue
|
||||
}
|
||||
|
||||
if (!localeEntry.isDirectory()) {
|
||||
throw new QuestValidationError(
|
||||
`${locale} exists but is not a directory`,
|
||||
id,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
const localePath = path.join(questPath, locale)
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(localePath)
|
||||
|
||||
if (!files.includes('game.yml')) {
|
||||
throw new QuestValidationError(
|
||||
`Missing ${locale}/game.yml`,
|
||||
id,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
const body = await fs.readFile(
|
||||
path.join(questPath, 'game.yml'),
|
||||
path.join(localePath, 'game.yml'),
|
||||
'utf-8'
|
||||
)
|
||||
|
||||
@@ -99,26 +136,74 @@ class QuestManager {
|
||||
merge: true,
|
||||
})
|
||||
|
||||
const image = files.includes('Dockerfile') ? id : 'base'
|
||||
const image = files.includes('Dockerfile')
|
||||
? questImageId(id, locale)
|
||||
: 'base'
|
||||
const quest = questSchema.parse({
|
||||
id,
|
||||
image,
|
||||
...questData,
|
||||
})
|
||||
quest.exceptions = [...exceptions]
|
||||
this.quests.set(id, quest)
|
||||
versions.set(locale, quest)
|
||||
} catch (error) {
|
||||
logger.error(`Error parsing quest ${id}`, { error })
|
||||
throw new QuestValidationError(`Error parsing quest ${id}`, id, error)
|
||||
logger.error(`Error parsing quest ${id} for locale ${locale}`, {
|
||||
error,
|
||||
})
|
||||
throw new QuestValidationError(
|
||||
`Error parsing quest ${id} for locale ${locale}`,
|
||||
id,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (versions.size > 0) {
|
||||
loaded.set(id, versions)
|
||||
} else {
|
||||
logger.warn(`No valid locales found for quest "${id}"`)
|
||||
}
|
||||
})
|
||||
|
||||
this.quests = loaded
|
||||
}
|
||||
|
||||
async getQuestDockerfiles() {
|
||||
const questlist = await this.mapQuests(async (id, questPath) => {
|
||||
const files = await fs.readdir(questPath)
|
||||
return files.includes('Dockerfile') ? { id, questPath } : null
|
||||
const entries = await fs.readdir(questPath, { withFileTypes: true })
|
||||
const dockerfiles: { id: string; questPath: string }[] = []
|
||||
|
||||
for (const locale of SUPPORTED_LOCALES) {
|
||||
const localeEntry = entries.find((entry) => entry.name === locale)
|
||||
|
||||
if (!localeEntry) {
|
||||
logger.warn(`Missing locale "${locale}" for quest "${id}"`)
|
||||
// eslint-disable-next-line no-continue
|
||||
continue
|
||||
}
|
||||
|
||||
if (!localeEntry.isDirectory()) {
|
||||
throw new QuestValidationError(
|
||||
`${locale} exists but is not a directory`,
|
||||
id,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
const localePath = path.join(questPath, locale)
|
||||
const files = await fs.readdir(localePath)
|
||||
|
||||
if (files.includes('Dockerfile')) {
|
||||
dockerfiles.push({
|
||||
id: questImageId(id, locale),
|
||||
questPath: localePath,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return dockerfiles.length > 0 ? dockerfiles : null
|
||||
})
|
||||
|
||||
return questlist.filter((quest) => quest !== null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { HydratedDocument } from 'mongoose'
|
||||
import type { QuestLocale } from '../../../packages/constants'
|
||||
import { LOCALE_NAME_MAP } from '../../../packages/constants'
|
||||
import { Session, UserProfile } from '../../../packages/models'
|
||||
import type { ISession, IUser } from '../../../packages/models'
|
||||
import {
|
||||
@@ -34,11 +36,12 @@ async function deactivateSessions(userId: string, quest: string) {
|
||||
|
||||
export async function createNewSession(
|
||||
userId: string,
|
||||
questId: string
|
||||
questId: string,
|
||||
locale: QuestLocale
|
||||
): Promise<ISession> {
|
||||
const quest = await questManager.get(questId)
|
||||
const quest = await questManager.get(questId, locale)
|
||||
if (!quest) {
|
||||
throw new Error(`Quest ${questId} not found`)
|
||||
throw new Error(`Quest ${questId} not found for ${LOCALE_NAME_MAP[locale]}`)
|
||||
}
|
||||
|
||||
// deactivate all active sessions
|
||||
@@ -55,12 +58,18 @@ export async function createNewSession(
|
||||
|
||||
const containerName = `quest-${quest.id}-${userProfile.user.username}-${Date.now()}`
|
||||
|
||||
const container = await createContainer(containerName, quest.id, quest.image)
|
||||
const container = await createContainer(
|
||||
containerName,
|
||||
quest.id,
|
||||
locale,
|
||||
quest.image
|
||||
)
|
||||
await getAndStartContainer(container.id)
|
||||
|
||||
const newSession = new Session({
|
||||
user: userId,
|
||||
quest: quest.id,
|
||||
locale,
|
||||
containerId: container.id,
|
||||
containerName,
|
||||
})
|
||||
@@ -117,15 +126,16 @@ export async function finishSession(
|
||||
|
||||
export async function isQuestUnlocked(
|
||||
userId: string,
|
||||
questId: string
|
||||
questId: string,
|
||||
locale: QuestLocale
|
||||
): Promise<boolean> {
|
||||
const userProfile = await UserProfile.findOne({ user: userId })
|
||||
if (!userProfile) {
|
||||
throw new Error(`UserProfile ${userId} not found`)
|
||||
}
|
||||
const quest = await questManager.get(questId)
|
||||
const quest = await questManager.get(questId, locale)
|
||||
if (!quest) {
|
||||
throw new Error(`Quest ${questId} not found`)
|
||||
throw new Error(`Quest ${questId} not found for ${LOCALE_NAME_MAP[locale]}`)
|
||||
}
|
||||
if (!quest.requirements) {
|
||||
return true
|
||||
|
||||
@@ -2,19 +2,27 @@ import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { router, publicProcedure } from '../trpc.js'
|
||||
import { questManager } from '../models/quest.js'
|
||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
||||
|
||||
export const questRouter = router({
|
||||
getQuests: publicProcedure.query(async () => {
|
||||
const quests = questManager.getAll()
|
||||
getQuests: publicProcedure
|
||||
.input(z.object({ locale: z.enum(SUPPORTED_LOCALES) }))
|
||||
.query(async ({ input }) => {
|
||||
const quests = questManager.getAll(input.locale)
|
||||
|
||||
return quests.map(({ title, id, requirements }) => ({
|
||||
id,
|
||||
title,
|
||||
requirements,
|
||||
}))
|
||||
}),
|
||||
getQuestDetail: publicProcedure.input(z.string()).query(async (opts) => {
|
||||
const questId = opts.input
|
||||
const quest = questManager.get(questId)
|
||||
|
||||
getQuestDetail: publicProcedure
|
||||
.input(z.object({ questId: z.string(), locale: z.enum(SUPPORTED_LOCALES) }))
|
||||
.query(async ({ input }) => {
|
||||
const { questId, locale } = input
|
||||
const quest = questManager.get(questId, locale)
|
||||
|
||||
if (!quest) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
||||
import { ISession, Session } from '../../../packages/models'
|
||||
import { GameSession, VoidFileExistenceChecker } from '../../../packages/game'
|
||||
import { createNewSession, isQuestUnlocked } from '../models/sessionManager.js'
|
||||
@@ -11,7 +12,7 @@ import { genJWT } from '../utils/auth'
|
||||
export type SessionDetail = Awaited<ReturnType<typeof sessionDetail>>
|
||||
|
||||
async function sessionDetail(session: ISession) {
|
||||
const quest = questManager.get(session.quest)
|
||||
const quest = questManager.get(session.quest, session.locale)
|
||||
if (!quest) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
@@ -31,6 +32,7 @@ async function sessionDetail(session: ISession) {
|
||||
_id: session._id.toString(),
|
||||
user: session.user.toString(),
|
||||
quest: session.quest,
|
||||
locale: session.locale,
|
||||
status: session.status,
|
||||
createdAt: session.createdAt.toISOString(),
|
||||
lastActivityAt: session.lastActivityAt.toISOString(),
|
||||
@@ -45,12 +47,20 @@ async function sessionDetail(session: ISession) {
|
||||
|
||||
export const sessionRouter = router({
|
||||
createSession: protectedProcedure
|
||||
.input(z.object({ questId: z.string() }))
|
||||
.input(
|
||||
z.object({
|
||||
questId: z.string(),
|
||||
locale: z.enum(SUPPORTED_LOCALES),
|
||||
})
|
||||
)
|
||||
.mutation(async (opts) => {
|
||||
const { questId } = opts.input
|
||||
const { questId, locale } = opts.input
|
||||
const { user } = opts.ctx
|
||||
if ((await isQuestUnlocked(user.id, questId)) || config.testing.enabled) {
|
||||
const session = await createNewSession(user.id, questId)
|
||||
if (
|
||||
(await isQuestUnlocked(user.id, questId, locale)) ||
|
||||
config.testing.enabled
|
||||
) {
|
||||
const session = await createNewSession(user.id, questId, locale)
|
||||
return sessionDetail(session)
|
||||
}
|
||||
throw new TRPCError({
|
||||
|
||||
Reference in new issue
Block a user