mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-04 16:22:48 +08:00
Add mission planning flow to learn page
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553
|
||||
size 68
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553
|
||||
size 68
|
||||
3
public/img/learn/missions/full-flight/briefing-hero.png
Normal file
3
public/img/learn/missions/full-flight/briefing-hero.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553
|
||||
size 68
|
||||
3
public/img/learn/missions/full-flight/briefing-route.png
Normal file
3
public/img/learn/missions/full-flight/briefing-route.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553
|
||||
size 68
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553
|
||||
size 68
|
||||
68
server/api/learn/simbrief.get.ts
Normal file
68
server/api/learn/simbrief.get.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { createError, defineEventHandler, getQuery } from 'h3'
|
||||
|
||||
const SIMBRIEF_TIMEOUT_MS = 10000
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const query = getQuery(event)
|
||||
const rawId = (query.userId ?? query.userid ?? '').toString().trim()
|
||||
if (!rawId) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'userId required' })
|
||||
}
|
||||
|
||||
const url = new URL('https://www.simbrief.com/api/xml.fetcher.php')
|
||||
url.searchParams.set('userid', rawId)
|
||||
url.searchParams.set('json', '1')
|
||||
|
||||
let response: Response
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort()
|
||||
}, SIMBRIEF_TIMEOUT_MS)
|
||||
try {
|
||||
response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'OpenSquawk Mission Planner'
|
||||
},
|
||||
cache: 'no-store',
|
||||
signal: controller.signal
|
||||
})
|
||||
} catch (error: any) {
|
||||
const aborted = error?.name === 'AbortError'
|
||||
console.error(
|
||||
`[simbrief] Request failed for user ${rawId}${aborted ? ' (timeout)' : ''}`,
|
||||
error
|
||||
)
|
||||
throw createError({
|
||||
statusCode: 502,
|
||||
statusMessage: aborted ? 'SimBrief request timed out' : 'SimBrief request failed',
|
||||
data: { message: aborted ? 'Request timed out' : error?.message || 'Network error' }
|
||||
})
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text().catch(() => '')
|
||||
console.error(
|
||||
`[simbrief] Request rejected for user ${rawId}: ${response.status} ${response.statusText}`,
|
||||
payload
|
||||
)
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: 'SimBrief request rejected',
|
||||
data: { message: payload || response.statusText }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await response.json()
|
||||
return { data }
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: 502,
|
||||
statusMessage: 'Invalid SimBrief response',
|
||||
data: { message: error?.message || 'Failed to parse JSON' }
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createBaseScenario, createScenarioSeries, formatTemp } from '~~/shared/learn/scenario'
|
||||
import type { ModuleDef } from '~~/shared/learn/types'
|
||||
import type { ModuleDef, Scenario } from '~~/shared/learn/types'
|
||||
|
||||
function gradientArt(colors: string[]): string {
|
||||
const stops = colors
|
||||
@@ -2194,6 +2194,18 @@ const decisionTreeLessons = [
|
||||
|
||||
const fullFlightScenario = createScenarioSeries()
|
||||
|
||||
export function seedFullFlightScenario(scenario: Scenario | null) {
|
||||
if (scenario) {
|
||||
fullFlightScenario.setScenario(scenario)
|
||||
} else {
|
||||
fullFlightScenario.setScenario(null)
|
||||
}
|
||||
}
|
||||
|
||||
export function peekFullFlightScenario(): Scenario | null {
|
||||
return fullFlightScenario.peek()
|
||||
}
|
||||
|
||||
const makeFullFlightGenerator = (reset = false) => () => {
|
||||
if (reset) {
|
||||
fullFlightScenario.reset()
|
||||
@@ -2951,7 +2963,11 @@ export const learnModules: ModuleDef[] = [
|
||||
title: 'Full Flight · Gate to Gate',
|
||||
subtitle: 'One linked scenario from clearance to taxi-in',
|
||||
art: '/img/learn/modules/img6.jpeg',
|
||||
lessons: fullFlightLessons
|
||||
lessons: fullFlightLessons,
|
||||
meta: {
|
||||
flightPlan: true,
|
||||
briefingArt: '/img/learn/missions/full-flight/briefing-hero.png'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ function generateSquawk(): string {
|
||||
return code
|
||||
}
|
||||
|
||||
function digitsToWords(value: string): string {
|
||||
export function digitsToWords(value: string): string {
|
||||
return value
|
||||
.split('')
|
||||
.map(char => atcNumberWords[char] ?? char)
|
||||
@@ -207,7 +207,7 @@ function digitsToWords(value: string): string {
|
||||
.trim()
|
||||
}
|
||||
|
||||
function lettersToNato(value: string): string {
|
||||
export function lettersToNato(value: string): string {
|
||||
return value
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
@@ -215,7 +215,7 @@ function lettersToNato(value: string): string {
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function runwayToWords(runway: string): string {
|
||||
export function runwayToWords(runway: string): string {
|
||||
const digits = runway.replace(/[^0-9]/g, '').padStart(2, '0')
|
||||
const suffix = runway.replace(/[0-9]/g, '')
|
||||
const base = digits
|
||||
@@ -274,17 +274,17 @@ function visibilityToWords(vis: string): string {
|
||||
return vis
|
||||
}
|
||||
|
||||
function minutesToWords(minutes: number): string {
|
||||
export function minutesToWords(minutes: number): string {
|
||||
const value = Math.max(1, Math.round(minutes))
|
||||
const unit = value === 1 ? 'minute' : 'minutes'
|
||||
return `${digitsToWords(value.toString())} ${unit}`
|
||||
}
|
||||
|
||||
function speedToWords(speed: number): string {
|
||||
export function speedToWords(speed: number): string {
|
||||
return `${digitsToWords(Math.round(speed).toString())} knots`
|
||||
}
|
||||
|
||||
function altitudeToWords(value: number): string {
|
||||
export function altitudeToWords(value: number): string {
|
||||
const thousands = Math.floor(value / 1000)
|
||||
const remainder = value % 1000
|
||||
let words = thousands ? `${digitsToWords(thousands.toString())} thousand` : ''
|
||||
@@ -491,18 +491,38 @@ function cloneScenario(scenario: Scenario): Scenario {
|
||||
return JSON.parse(JSON.stringify(scenario)) as Scenario
|
||||
}
|
||||
|
||||
export type ScenarioGenerator = (() => Scenario) & { reset: () => void }
|
||||
export type ScenarioGenerator = (() => Scenario) & {
|
||||
reset: () => void
|
||||
setScenario: (scenario: Scenario | null) => void
|
||||
peek: () => Scenario | null
|
||||
}
|
||||
|
||||
export function createScenarioSeries(): ScenarioGenerator {
|
||||
export function createScenarioSeries(source?: () => Scenario): ScenarioGenerator {
|
||||
let cached: Scenario | null = null
|
||||
const generator = () => {
|
||||
let override: Scenario | null = null
|
||||
const resolveSource = () => {
|
||||
if (override) {
|
||||
return cloneScenario(override)
|
||||
}
|
||||
return source ? source() : createBaseScenario()
|
||||
}
|
||||
const generator = (() => {
|
||||
if (!cached) {
|
||||
cached = createBaseScenario()
|
||||
cached = resolveSource()
|
||||
}
|
||||
return cloneScenario(cached)
|
||||
}
|
||||
}) as ScenarioGenerator
|
||||
generator.reset = () => {
|
||||
cached = null
|
||||
cached = override ? cloneScenario(override) : null
|
||||
}
|
||||
generator.setScenario = (scenario: Scenario | null) => {
|
||||
override = scenario ? cloneScenario(scenario) : null
|
||||
cached = override ? cloneScenario(override) : null
|
||||
}
|
||||
generator.peek = () => {
|
||||
if (cached) return cloneScenario(cached)
|
||||
if (override) return cloneScenario(override)
|
||||
return null
|
||||
}
|
||||
return generator
|
||||
}
|
||||
|
||||
@@ -161,10 +161,16 @@ export type Lesson = {
|
||||
generate: () => Scenario
|
||||
}
|
||||
|
||||
export type ModuleMeta = {
|
||||
flightPlan?: boolean
|
||||
briefingArt?: string
|
||||
}
|
||||
|
||||
export type ModuleDef = {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
art: string
|
||||
lessons: Lesson[]
|
||||
meta?: ModuleMeta
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user