feat(client): display semantic asset resource type instead of filename during boot
This commit is contained in:
parent
309738cf79
commit
5977d81c2d
|
|
@ -377,7 +377,7 @@
|
||||||
|
|
||||||
#d2-boot-progress-file {
|
#d2-boot-progress-file {
|
||||||
color: #f0dfb8;
|
color: #f0dfb8;
|
||||||
font-family: monospace;
|
font-family: var(--font-body);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
max-width: 290px;
|
max-width: 290px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -422,7 +422,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="d2-boot-meta">
|
<div class="d2-boot-meta">
|
||||||
<span class="d2-boot-file-label">正在下载:</span>
|
<span class="d2-boot-file-label">正在下载:</span>
|
||||||
<span id="d2-boot-progress-file">initializing...</span>
|
<span id="d2-boot-progress-file">正在初始化...</span>
|
||||||
<span id="d2-boot-progress-count">(0/0)</span>
|
<span id="d2-boot-progress-count">(0/0)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ async function bootPlayApp(): Promise<PlayAppController> {
|
||||||
onPanAutomap: (dx, dy) => minimap.pan(dx, dy),
|
onPanAutomap: (dx, dy) => minimap.pan(dx, dy),
|
||||||
})
|
})
|
||||||
await hudManager.load('/ui')
|
await hudManager.load('/ui')
|
||||||
bootProgress.setStageFloor(25, 'manifest.json')
|
bootProgress.setStageFloor(25, '游戏界面资源')
|
||||||
|
|
||||||
// Fail fast: without the packed act tiles there is no 1.13c map (automap, collision, picking).
|
// Fail fast: without the packed act tiles there is no 1.13c map (automap, collision, picking).
|
||||||
let mapService: MapService
|
let mapService: MapService
|
||||||
|
|
@ -247,7 +247,7 @@ async function bootPlayApp(): Promise<PlayAppController> {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`MapService: failed to load /act-packs/tiles: ${err instanceof Error ? err.message : String(err)}`)
|
throw new Error(`MapService: failed to load /act-packs/tiles: ${err instanceof Error ? err.message : String(err)}`)
|
||||||
}
|
}
|
||||||
bootProgress.setStageFloor(45, 'drlg-bundle.bin')
|
bootProgress.setStageFloor(45, '地图地形资源')
|
||||||
|
|
||||||
// Kick off asynchronous GPU atlas preloading for WebGL2 world rendering
|
// Kick off asynchronous GPU atlas preloading for WebGL2 world rendering
|
||||||
tileGpuCache.ensureActPalette(1)
|
tileGpuCache.ensureActPalette(1)
|
||||||
|
|
@ -679,9 +679,9 @@ async function bootPlayApp(): Promise<PlayAppController> {
|
||||||
})
|
})
|
||||||
|
|
||||||
await frontendRenderer.load('/ui/frontend')
|
await frontendRenderer.load('/ui/frontend')
|
||||||
bootProgress.setStageFloor(78, 'bg-main-menu.png')
|
bootProgress.setStageFloor(78, '主菜单与大厅界面')
|
||||||
await assetsReadyPromise
|
await assetsReadyPromise
|
||||||
bootProgress.setStageFloor(96, 'items-atlas.png')
|
bootProgress.setStageFloor(96, '物品图标资源')
|
||||||
await bootProgress.waitForIdle(2500)
|
await bootProgress.waitForIdle(2500)
|
||||||
bootProgress.complete()
|
bootProgress.complete()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,16 @@
|
||||||
*
|
*
|
||||||
* Renders:
|
* Renders:
|
||||||
* 1. Total download progress percentage (`0%` – `100%`, strictly monotonic).
|
* 1. Total download progress percentage (`0%` – `100%`, strictly monotonic).
|
||||||
* 2. The filename of the resource currently being downloaded (`currentFile`).
|
* 2. The semantic resource type of the asset currently being downloaded (`currentFile` / `currentResourceType`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface BootProgressSnapshot {
|
export interface BootProgressSnapshot {
|
||||||
/** Monotonic total download percentage in `[0, 100]`. */
|
/** Monotonic total download percentage in `[0, 100]`. */
|
||||||
readonly percent: number
|
readonly percent: number
|
||||||
/** Basename of the resource currently being downloaded (e.g. `drlg-bundle.bin`, `items-atlas.png`). */
|
/** Semantic resource type of the resource currently being downloaded (e.g. `地图地形资源`, `物品图标资源`). */
|
||||||
readonly currentFile: string
|
readonly currentFile: string
|
||||||
|
/** Semantic resource type of the resource currently being downloaded. */
|
||||||
|
readonly currentResourceType: string
|
||||||
/** Normalized URL/path of the resource currently being downloaded. */
|
/** Normalized URL/path of the resource currently being downloaded. */
|
||||||
readonly currentFilePath: string
|
readonly currentFilePath: string
|
||||||
/** Number of resources that have finished downloading. */
|
/** Number of resources that have finished downloading. */
|
||||||
|
|
@ -26,7 +28,7 @@ export interface BootProgressSnapshot {
|
||||||
readonly inFlightCount: number
|
readonly inFlightCount: number
|
||||||
/** Lifecycle status of the boot progress tracker. */
|
/** Lifecycle status of the boot progress tracker. */
|
||||||
readonly status: 'idle' | 'loading' | 'completed'
|
readonly status: 'idle' | 'loading' | 'completed'
|
||||||
/** Ordered list of resource filenames that have completed downloading. */
|
/** Ordered list of resource types that have completed downloading. */
|
||||||
readonly loadedFiles: readonly string[]
|
readonly loadedFiles: readonly string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,7 +37,7 @@ export interface BootProgressOptions {
|
||||||
readonly stageEl?: HTMLElement | null
|
readonly stageEl?: HTMLElement | null
|
||||||
/** Baseline expected number of boot resource requests used to prevent early 100% spikes. */
|
/** Baseline expected number of boot resource requests used to prevent early 100% spikes. */
|
||||||
readonly expectedTotal?: number
|
readonly expectedTotal?: number
|
||||||
/** Initial placeholder filename before the first request starts. */
|
/** Initial placeholder label before the first request starts. */
|
||||||
readonly initialFileLabel?: string
|
readonly initialFileLabel?: string
|
||||||
/** Whether to automatically hide the overlay when `complete()` is called (default: `true`). */
|
/** Whether to automatically hide the overlay when `complete()` is called (default: `true`). */
|
||||||
readonly hideOnComplete?: boolean
|
readonly hideOnComplete?: boolean
|
||||||
|
|
@ -44,6 +46,7 @@ export interface BootProgressOptions {
|
||||||
interface InFlightEntry {
|
interface InFlightEntry {
|
||||||
readonly id: number
|
readonly id: number
|
||||||
readonly fileName: string
|
readonly fileName: string
|
||||||
|
readonly resourceType: string
|
||||||
readonly filePath: string
|
readonly filePath: string
|
||||||
readonly promise: Promise<void>
|
readonly promise: Promise<void>
|
||||||
readonly resolve: () => void
|
readonly resolve: () => void
|
||||||
|
|
@ -58,9 +61,29 @@ interface InFlightEntry {
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_EXPECTED_BOOT_ASSETS = 910
|
export const DEFAULT_EXPECTED_BOOT_ASSETS = 910
|
||||||
|
|
||||||
|
const INITIAL_PLACEHOLDER_LABELS = new Set(['initializing...', '正在初始化...'])
|
||||||
|
|
||||||
|
const KNOWN_RESOURCE_TYPES = new Set([
|
||||||
|
'字体资源',
|
||||||
|
'小地图资源',
|
||||||
|
'技能图标资源',
|
||||||
|
'物品图标资源',
|
||||||
|
'投射物特效资源',
|
||||||
|
'光环与状态特效',
|
||||||
|
'角色美术资源',
|
||||||
|
'怪物美术资源',
|
||||||
|
'地图地形资源',
|
||||||
|
'主菜单与大厅界面',
|
||||||
|
'游戏界面资源',
|
||||||
|
'音频资源',
|
||||||
|
'资源配置数据',
|
||||||
|
'美术贴图资源',
|
||||||
|
'游戏资源',
|
||||||
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts a clean resource filename from a URL or relative path.
|
* Extracts a clean resource filename from a URL or relative path.
|
||||||
* Strips query parameters (`?...`) and hash fragments (`#...`).
|
* Strips query parameters and hash fragments.
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
* - `/act-packs/tiles/drlg-bundle.bin` -> `drlg-bundle.bin`
|
* - `/act-packs/tiles/drlg-bundle.bin` -> `drlg-bundle.bin`
|
||||||
|
|
@ -108,6 +131,133 @@ export function normalizeAssetPath(urlOrPath: string): string {
|
||||||
return noQuery.replace(/\\/g, '/')
|
return noQuery.replace(/\\/g, '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classifies a resource URL or path into a human-readable semantic asset type label
|
||||||
|
* (replacing raw filenames in the homepage boot download progress UI).
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - `/ui/fonts/metrics.json` -> `'字体资源'`
|
||||||
|
* - `/act-packs/tiles/drlg-bundle.bin` -> `'地图地形资源'`
|
||||||
|
* - `/ui/frontend/bg-main-menu.png` -> `'主菜单与大厅界面'`
|
||||||
|
* - `samples/d2-packs/items/items-atlas.png` -> `'物品图标资源'`
|
||||||
|
* - `samples/d2-packs/entities/char-so.r8` -> `'角色美术资源'`
|
||||||
|
* - `samples/d2-packs/entities/monster-fallen1-hth.r8` -> `'怪物美术资源'`
|
||||||
|
* - `/missiles/firebolt.png` -> `'投射物特效资源'`
|
||||||
|
* - `/overlays/AuraDefianceBack.png` -> `'光环与状态特效'`
|
||||||
|
* - `/skills/icon_36.png` -> `'技能图标资源'`
|
||||||
|
* - `/assets/ui/automap/MaxiMap.png` -> `'小地图资源'`
|
||||||
|
* - `/ui/ctrlpnl-800.png` -> `'游戏界面资源'`
|
||||||
|
*/
|
||||||
|
export function extractAssetResourceType(urlOrPath: string): string {
|
||||||
|
const trimmed = urlOrPath.trim()
|
||||||
|
if (!trimmed || trimmed.startsWith('data:') || trimmed.startsWith('blob:')) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (KNOWN_RESOURCE_TYPES.has(trimmed)) {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawFileName = extractAssetFileName(trimmed)
|
||||||
|
if (!rawFileName) return ''
|
||||||
|
|
||||||
|
const fileName = rawFileName.toLowerCase()
|
||||||
|
const normPath = normalizeAssetPath(trimmed).toLowerCase()
|
||||||
|
|
||||||
|
if (
|
||||||
|
normPath.includes('/fonts/') ||
|
||||||
|
normPath.startsWith('fonts/') ||
|
||||||
|
fileName === 'metrics.json' ||
|
||||||
|
fileName.startsWith('font') ||
|
||||||
|
/\.(?:ttf|woff2?|otf)$/i.test(fileName)
|
||||||
|
) {
|
||||||
|
return '字体资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normPath.includes('/automap/') ||
|
||||||
|
/^(?:automap|maximap|act[24]map|extnmap)/i.test(fileName)
|
||||||
|
) {
|
||||||
|
return '小地图资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normPath.includes('/skills/') ||
|
||||||
|
normPath.startsWith('skills/') ||
|
||||||
|
/^(?:skill|generic-skills|bg_(?:ama|ass|bar|dru|nec|pal|sor)_)/i.test(fileName)
|
||||||
|
) {
|
||||||
|
return '技能图标资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normPath.includes('/items/') || fileName.startsWith('items-atlas')) {
|
||||||
|
return '物品图标资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normPath.includes('/missiles/') || normPath.startsWith('missiles/')) {
|
||||||
|
return '投射物特效资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normPath.includes('/overlays/') || normPath.startsWith('overlays/')) {
|
||||||
|
return '光环与状态特效'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normPath.includes('/chars/') || /^char-(?!sheet|select)/i.test(fileName)) {
|
||||||
|
return '角色美术资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normPath.includes('/entities/') || /^(?:monster-|monstats)/i.test(fileName)) {
|
||||||
|
return '怪物美术资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normPath.includes('/tiles/') ||
|
||||||
|
normPath.startsWith('act-packs/tiles') ||
|
||||||
|
/(?:^|\/)packs\/act[1-5]\//i.test(normPath) ||
|
||||||
|
/^(?:drlg-bundle|palette\.bin|world-graph|tiles-|objects-|scene\.json)/i.test(fileName) ||
|
||||||
|
/\.(?:dt1|ds1|pal|pl2)$/i.test(fileName)
|
||||||
|
) {
|
||||||
|
return '地图地形资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normPath.includes('/ui/frontend/') ||
|
||||||
|
normPath.startsWith('ui/frontend/') ||
|
||||||
|
/^(?:bg-main-menu|bg-char-create|bg-char-select|bg-waiting-room|loading-door-strip|create-game-bg|charselectbox|realmselect|popup-|logo-|gembutton|textbox|clickbox|joingameclickbox|radiobutton|grey-level-box|btn-)/i.test(
|
||||||
|
fileName,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return '主菜单与大厅界面'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normPath.includes('/ui/') ||
|
||||||
|
normPath.startsWith('ui/') ||
|
||||||
|
/^(?:ctrlpnl|globe-|overlap-|border-|char-sheet|inv-sheet|inv-tab|quest-|quests-atlas|waypoint-|stash-bg|cube-bg|vendor-|popbelt|minipanel|runbutton|menubutton|buysellbtn|goldcoinbtn|miniconvert|level-btn|level-socket|cursor|boxpieces|focus16)/i.test(
|
||||||
|
fileName,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return '游戏界面资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normPath.includes('/audio/') ||
|
||||||
|
normPath.includes('/sound/') ||
|
||||||
|
normPath.includes('/music/') ||
|
||||||
|
/\.(?:mp3|ogg|wav)$/i.test(fileName)
|
||||||
|
) {
|
||||||
|
return '音频资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normPath.includes('/anim/') || /\.(?:json|bin|d2|txt|tbl|dat)$/i.test(fileName)) {
|
||||||
|
return '资源配置数据'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\.(?:png|r8|dc6|dcc|pcx|cel)$/i.test(fileName)) {
|
||||||
|
return '美术贴图资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '游戏资源'
|
||||||
|
}
|
||||||
|
|
||||||
export class BootProgressTracker {
|
export class BootProgressTracker {
|
||||||
private readonly stageEl: HTMLElement | null
|
private readonly stageEl: HTMLElement | null
|
||||||
private readonly expectedTotal: number
|
private readonly expectedTotal: number
|
||||||
|
|
@ -139,7 +289,7 @@ export class BootProgressTracker {
|
||||||
constructor(options: BootProgressOptions = {}) {
|
constructor(options: BootProgressOptions = {}) {
|
||||||
this.stageEl = options.stageEl ?? null
|
this.stageEl = options.stageEl ?? null
|
||||||
this.expectedTotal = Math.max(1, options.expectedTotal ?? DEFAULT_EXPECTED_BOOT_ASSETS)
|
this.expectedTotal = Math.max(1, options.expectedTotal ?? DEFAULT_EXPECTED_BOOT_ASSETS)
|
||||||
this.currentFile = options.initialFileLabel ?? 'initializing...'
|
this.currentFile = options.initialFileLabel ?? '正在初始化...'
|
||||||
this.hideOnComplete = options.hideOnComplete ?? true
|
this.hideOnComplete = options.hideOnComplete ?? true
|
||||||
this.bindOrCreateDom()
|
this.bindOrCreateDom()
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +314,7 @@ export class BootProgressTracker {
|
||||||
' </div>',
|
' </div>',
|
||||||
' <div class="d2-boot-meta">',
|
' <div class="d2-boot-meta">',
|
||||||
' <span class="d2-boot-file-label">正在下载:</span>',
|
' <span class="d2-boot-file-label">正在下载:</span>',
|
||||||
' <span id="d2-boot-progress-file">initializing...</span>',
|
' <span id="d2-boot-progress-file">正在初始化...</span>',
|
||||||
' <span id="d2-boot-progress-count">(0/0)</span>',
|
' <span id="d2-boot-progress-count">(0/0)</span>',
|
||||||
' </div>',
|
' </div>',
|
||||||
'</div>',
|
'</div>',
|
||||||
|
|
@ -337,13 +487,14 @@ export class BootProgressTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records the start of a resource download and updates the active filename & percentage.
|
* Records the start of a resource download and updates the active resource type & percentage.
|
||||||
* Returns a non-zero tracking ID (or `0` if the URL is ignored, e.g. `data:`/`blob:`).
|
* Returns a non-zero tracking ID (or `0` if the URL is ignored, e.g. `data:`/`blob:`).
|
||||||
*/
|
*/
|
||||||
beginFile(urlOrPath: string): number {
|
beginFile(urlOrPath: string): number {
|
||||||
if (this.status === 'completed') return 0
|
if (this.status === 'completed') return 0
|
||||||
const fileName = extractAssetFileName(urlOrPath)
|
const fileName = extractAssetFileName(urlOrPath)
|
||||||
if (!fileName) return 0
|
if (!fileName) return 0
|
||||||
|
const resourceType = extractAssetResourceType(urlOrPath) || '游戏资源'
|
||||||
|
|
||||||
const filePath = normalizeAssetPath(urlOrPath) || fileName
|
const filePath = normalizeAssetPath(urlOrPath) || fileName
|
||||||
if (this.status === 'idle') {
|
if (this.status === 'idle') {
|
||||||
|
|
@ -360,12 +511,13 @@ export class BootProgressTracker {
|
||||||
this.inFlight.set(id, {
|
this.inFlight.set(id, {
|
||||||
id,
|
id,
|
||||||
fileName,
|
fileName,
|
||||||
|
resourceType,
|
||||||
filePath,
|
filePath,
|
||||||
promise,
|
promise,
|
||||||
resolve: resolveFn,
|
resolve: resolveFn,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.currentFile = fileName
|
this.currentFile = resourceType
|
||||||
this.currentFilePath = filePath
|
this.currentFilePath = filePath
|
||||||
this.recalculatePercent()
|
this.recalculatePercent()
|
||||||
this.renderDom()
|
this.renderDom()
|
||||||
|
|
@ -385,21 +537,21 @@ export class BootProgressTracker {
|
||||||
if (this.status === 'completed') return
|
if (this.status === 'completed') return
|
||||||
|
|
||||||
this.loadedCount++
|
this.loadedCount++
|
||||||
this.loadedFiles.push(entry.fileName)
|
this.loadedFiles.push(entry.resourceType)
|
||||||
|
|
||||||
// If other files are still downloading, show the most recently started in-flight file;
|
// If other files are still downloading, show the most recently started in-flight resource type;
|
||||||
// otherwise keep the just-finished file name visible.
|
// otherwise keep the just-finished resource type visible.
|
||||||
if (this.inFlight.size > 0) {
|
if (this.inFlight.size > 0) {
|
||||||
let latest: InFlightEntry | undefined
|
let latest: InFlightEntry | undefined
|
||||||
for (const item of this.inFlight.values()) {
|
for (const item of this.inFlight.values()) {
|
||||||
latest = item
|
latest = item
|
||||||
}
|
}
|
||||||
if (latest) {
|
if (latest) {
|
||||||
this.currentFile = latest.fileName
|
this.currentFile = latest.resourceType
|
||||||
this.currentFilePath = latest.filePath
|
this.currentFilePath = latest.filePath
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.currentFile = entry.fileName
|
this.currentFile = entry.resourceType
|
||||||
this.currentFilePath = entry.filePath
|
this.currentFilePath = entry.filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,8 +583,8 @@ export class BootProgressTracker {
|
||||||
if (clamped > this.stageFloorPercent) {
|
if (clamped > this.stageFloorPercent) {
|
||||||
this.stageFloorPercent = clamped
|
this.stageFloorPercent = clamped
|
||||||
}
|
}
|
||||||
if (fallbackFile && (!this.currentFile || this.currentFile === 'initializing...')) {
|
if (fallbackFile && (!this.currentFile || INITIAL_PLACEHOLDER_LABELS.has(this.currentFile))) {
|
||||||
this.currentFile = extractAssetFileName(fallbackFile) || fallbackFile
|
this.currentFile = extractAssetResourceType(fallbackFile) || fallbackFile
|
||||||
this.currentFilePath = normalizeAssetPath(fallbackFile) || this.currentFile
|
this.currentFilePath = normalizeAssetPath(fallbackFile) || this.currentFile
|
||||||
}
|
}
|
||||||
this.recalculatePercent()
|
this.recalculatePercent()
|
||||||
|
|
@ -462,7 +614,7 @@ export class BootProgressTracker {
|
||||||
complete(): void {
|
complete(): void {
|
||||||
for (const entry of this.inFlight.values()) {
|
for (const entry of this.inFlight.values()) {
|
||||||
this.loadedCount++
|
this.loadedCount++
|
||||||
this.loadedFiles.push(entry.fileName)
|
this.loadedFiles.push(entry.resourceType)
|
||||||
entry.resolve()
|
entry.resolve()
|
||||||
}
|
}
|
||||||
this.inFlight.clear()
|
this.inFlight.clear()
|
||||||
|
|
@ -470,8 +622,8 @@ export class BootProgressTracker {
|
||||||
this.uninstall()
|
this.uninstall()
|
||||||
this.status = 'completed'
|
this.status = 'completed'
|
||||||
this.percent = 100
|
this.percent = 100
|
||||||
if (!this.currentFile || this.currentFile === 'initializing...') {
|
if (!this.currentFile || INITIAL_PLACEHOLDER_LABELS.has(this.currentFile)) {
|
||||||
this.currentFile = this.loadedFiles[this.loadedFiles.length - 1] ?? 'play.html'
|
this.currentFile = this.loadedFiles[this.loadedFiles.length - 1] ?? '游戏资源'
|
||||||
}
|
}
|
||||||
this.renderDom()
|
this.renderDom()
|
||||||
if (this.overlayEl) {
|
if (this.overlayEl) {
|
||||||
|
|
@ -508,7 +660,7 @@ export class BootProgressTracker {
|
||||||
}
|
}
|
||||||
if (this.fileEl) {
|
if (this.fileEl) {
|
||||||
this.fileEl.textContent = this.currentFile
|
this.fileEl.textContent = this.currentFile
|
||||||
this.fileEl.setAttribute('title', this.currentFilePath || this.currentFile)
|
this.fileEl.setAttribute('title', this.currentFile)
|
||||||
}
|
}
|
||||||
if (this.countEl) {
|
if (this.countEl) {
|
||||||
this.countEl.textContent = `(${this.loadedCount}/${totalDisplay})`
|
this.countEl.textContent = `(${this.loadedCount}/${totalDisplay})`
|
||||||
|
|
@ -530,12 +682,13 @@ export class BootProgressTracker {
|
||||||
return {
|
return {
|
||||||
percent: this.percent,
|
percent: this.percent,
|
||||||
currentFile: this.currentFile,
|
currentFile: this.currentFile,
|
||||||
|
currentResourceType: this.currentFile,
|
||||||
currentFilePath: this.currentFilePath,
|
currentFilePath: this.currentFilePath,
|
||||||
loadedCount: this.loadedCount,
|
loadedCount: this.loadedCount,
|
||||||
totalCount: Math.max(this.loadedCount, this.totalDiscovered),
|
totalCount: Math.max(this.loadedCount, this.totalDiscovered),
|
||||||
inFlightCount: this.inFlight.size,
|
inFlightCount: this.inFlight.size,
|
||||||
status: this.status,
|
status: this.status,
|
||||||
loadedFiles: [...this.loadedFiles],
|
loadedFiles: Array.from(this.loadedFiles),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||||
import {
|
import {
|
||||||
BootProgressTracker,
|
BootProgressTracker,
|
||||||
extractAssetFileName,
|
extractAssetFileName,
|
||||||
|
extractAssetResourceType,
|
||||||
normalizeAssetPath,
|
normalizeAssetPath,
|
||||||
type BootProgressSnapshot,
|
type BootProgressSnapshot,
|
||||||
} from '../src/client/ui/boot-progress.ts'
|
} from '../src/client/ui/boot-progress.ts'
|
||||||
|
|
@ -124,6 +125,32 @@ describe('Client Boot Resource Download Progress Bar (boot-progress)', () => {
|
||||||
expect(extractAssetFileName('')).toBe('')
|
expect(extractAssetFileName('')).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('extractAssetResourceType classifies asset paths into semantic resource types instead of raw filenames', () => {
|
||||||
|
expect(extractAssetResourceType('/ui/fonts/metrics.json')).toBe('字体资源')
|
||||||
|
expect(extractAssetResourceType('/ui/frontend/fonts/fontformal12-c4.png')).toBe('字体资源')
|
||||||
|
expect(extractAssetResourceType('/act-packs/tiles/drlg-bundle.bin')).toBe('地图地形资源')
|
||||||
|
expect(extractAssetResourceType('data\\global\\tiles\\act1\\town\\town.dt1')).toBe('地图地形资源')
|
||||||
|
expect(extractAssetResourceType('http://127.0.0.1:5173/ui/frontend/bg-main-menu.png#top')).toBe(
|
||||||
|
'主菜单与大厅界面',
|
||||||
|
)
|
||||||
|
expect(extractAssetResourceType('/ui/ctrlpnl-800.png')).toBe('游戏界面资源')
|
||||||
|
expect(extractAssetResourceType('samples/d2-packs/items/items-atlas.png?v=123')).toBe(
|
||||||
|
'物品图标资源',
|
||||||
|
)
|
||||||
|
expect(extractAssetResourceType('/skills/icon_36.png')).toBe('技能图标资源')
|
||||||
|
expect(extractAssetResourceType('/assets/ui/automap/MaxiMap.png')).toBe('小地图资源')
|
||||||
|
expect(extractAssetResourceType('samples/d2-packs/entities/char-so.r8')).toBe('角色美术资源')
|
||||||
|
expect(extractAssetResourceType('samples/d2-packs/entities/monster-fallen1-hth.r8')).toBe(
|
||||||
|
'怪物美术资源',
|
||||||
|
)
|
||||||
|
expect(extractAssetResourceType('/missiles/firebolt.png')).toBe('投射物特效资源')
|
||||||
|
expect(extractAssetResourceType('/overlays/AuraDefianceBack.png')).toBe('光环与状态特效')
|
||||||
|
expect(extractAssetResourceType('./asset-manifest.json')).toBe('资源配置数据')
|
||||||
|
expect(extractAssetResourceType('data:image/png;base64,iVBORw0KGgo=')).toBe('')
|
||||||
|
expect(extractAssetResourceType('blob:http://127.0.0.1:5173/abc-123')).toBe('')
|
||||||
|
expect(extractAssetResourceType('')).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
it('normalizeAssetPath strips origin, query, and hash while preserving path', () => {
|
it('normalizeAssetPath strips origin, query, and hash while preserving path', () => {
|
||||||
expect(normalizeAssetPath('http://127.0.0.1:5173/ui/frontend/bg-main-menu.png?t=1#a')).toBe(
|
expect(normalizeAssetPath('http://127.0.0.1:5173/ui/frontend/bg-main-menu.png?t=1#a')).toBe(
|
||||||
'/ui/frontend/bg-main-menu.png',
|
'/ui/frontend/bg-main-menu.png',
|
||||||
|
|
@ -131,7 +158,7 @@ describe('Client Boot Resource Download Progress Bar (boot-progress)', () => {
|
||||||
expect(normalizeAssetPath('/act-packs/tiles/index.json')).toBe('/act-packs/tiles/index.json')
|
expect(normalizeAssetPath('/act-packs/tiles/index.json')).toBe('/act-packs/tiles/index.json')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tracks downloading filenames, monotonic percentage (0% -> 100%), and updates DOM elements', () => {
|
it('tracks downloading resource types (instead of raw filenames), monotonic percentage (0% -> 100%), and updates DOM elements', () => {
|
||||||
const dom = createMockDomEnvironment()
|
const dom = createMockDomEnvironment()
|
||||||
cleanupDom = dom.cleanup
|
cleanupDom = dom.cleanup
|
||||||
|
|
||||||
|
|
@ -151,19 +178,21 @@ describe('Client Boot Resource Download Progress Bar (boot-progress)', () => {
|
||||||
expect(overlayEl.hidden).toBe(false)
|
expect(overlayEl.hidden).toBe(false)
|
||||||
expect(percentEl.textContent).toBe('0%')
|
expect(percentEl.textContent).toBe('0%')
|
||||||
|
|
||||||
// Start downloading file 1 & file 2 concurrently
|
// Start downloading file 1 & file 2 concurrently -> displays resource types instead of filenames
|
||||||
const id1 = tracker.beginFile('/ui/fonts/metrics.json')
|
const id1 = tracker.beginFile('/ui/fonts/metrics.json')
|
||||||
expect(tracker.getSnapshot().currentFile).toBe('metrics.json')
|
expect(tracker.getSnapshot().currentFile).toBe('字体资源')
|
||||||
expect(fileEl.textContent).toBe('metrics.json')
|
expect(tracker.getSnapshot().currentResourceType).toBe('字体资源')
|
||||||
|
expect(fileEl.textContent).toBe('字体资源')
|
||||||
|
expect(fileEl.getAttribute('title')).toBe('字体资源')
|
||||||
|
|
||||||
const id2 = tracker.beginFile('/act-packs/tiles/drlg-bundle.bin')
|
const id2 = tracker.beginFile('/act-packs/tiles/drlg-bundle.bin')
|
||||||
expect(tracker.getSnapshot().currentFile).toBe('drlg-bundle.bin')
|
expect(tracker.getSnapshot().currentFile).toBe('地图地形资源')
|
||||||
expect(fileEl.textContent).toBe('drlg-bundle.bin')
|
expect(fileEl.textContent).toBe('地图地形资源')
|
||||||
|
|
||||||
// Finish file 2 while file 1 is still in flight -> currentFile falls back to in-flight file 1
|
// Finish file 2 while file 1 is still in flight -> currentFile falls back to in-flight file 1's resource type
|
||||||
tracker.endFile(id2)
|
tracker.endFile(id2)
|
||||||
expect(tracker.getSnapshot().percent).toBe(25)
|
expect(tracker.getSnapshot().percent).toBe(25)
|
||||||
expect(tracker.getSnapshot().currentFile).toBe('metrics.json')
|
expect(tracker.getSnapshot().currentFile).toBe('字体资源')
|
||||||
expect(percentEl.textContent).toBe('25%')
|
expect(percentEl.textContent).toBe('25%')
|
||||||
expect(fillEl.style.width).toBe('25%')
|
expect(fillEl.style.width).toBe('25%')
|
||||||
|
|
||||||
|
|
@ -174,12 +203,12 @@ describe('Client Boot Resource Download Progress Bar (boot-progress)', () => {
|
||||||
|
|
||||||
// Start & finish file 3 & file 4
|
// Start & finish file 3 & file 4
|
||||||
const id3 = tracker.beginFile('/ui/frontend/bg-main-menu.png')
|
const id3 = tracker.beginFile('/ui/frontend/bg-main-menu.png')
|
||||||
expect(fileEl.textContent).toBe('bg-main-menu.png')
|
expect(fileEl.textContent).toBe('主菜单与大厅界面')
|
||||||
tracker.endFile(id3)
|
tracker.endFile(id3)
|
||||||
expect(tracker.getSnapshot().percent).toBe(75)
|
expect(tracker.getSnapshot().percent).toBe(75)
|
||||||
|
|
||||||
const id4 = tracker.beginFile('samples/d2-packs/items/items-atlas.png')
|
const id4 = tracker.beginFile('samples/d2-packs/items/items-atlas.png')
|
||||||
expect(fileEl.textContent).toBe('items-atlas.png')
|
expect(fileEl.textContent).toBe('物品图标资源')
|
||||||
tracker.endFile(id4)
|
tracker.endFile(id4)
|
||||||
expect(tracker.getSnapshot().percent).toBe(99)
|
expect(tracker.getSnapshot().percent).toBe(99)
|
||||||
|
|
||||||
|
|
@ -190,10 +219,10 @@ describe('Client Boot Resource Download Progress Bar (boot-progress)', () => {
|
||||||
expect(finalSnap.status).toBe('completed')
|
expect(finalSnap.status).toBe('completed')
|
||||||
expect(finalSnap.loadedCount).toBe(4)
|
expect(finalSnap.loadedCount).toBe(4)
|
||||||
expect(finalSnap.loadedFiles).toEqual([
|
expect(finalSnap.loadedFiles).toEqual([
|
||||||
'drlg-bundle.bin',
|
'地图地形资源',
|
||||||
'metrics.json',
|
'字体资源',
|
||||||
'bg-main-menu.png',
|
'主菜单与大厅界面',
|
||||||
'items-atlas.png',
|
'物品图标资源',
|
||||||
])
|
])
|
||||||
expect(percentEl.textContent).toBe('100%')
|
expect(percentEl.textContent).toBe('100%')
|
||||||
expect(fillEl.style.width).toBe('100%')
|
expect(fillEl.style.width).toBe('100%')
|
||||||
|
|
@ -228,13 +257,13 @@ describe('Client Boot Resource Download Progress Bar (boot-progress)', () => {
|
||||||
expect(globalThis.fetch).not.toBe(mockFetch)
|
expect(globalThis.fetch).not.toBe(mockFetch)
|
||||||
|
|
||||||
const res1 = await globalThis.fetch('/act-packs/tiles/index.json')
|
const res1 = await globalThis.fetch('/act-packs/tiles/index.json')
|
||||||
expect(tracker.getSnapshot().currentFile).toBe('index.json')
|
expect(tracker.getSnapshot().currentFile).toBe('地图地形资源')
|
||||||
await res1.json()
|
await res1.json()
|
||||||
expect(tracker.getSnapshot().loadedCount).toBe(1)
|
expect(tracker.getSnapshot().loadedCount).toBe(1)
|
||||||
expect(tracker.getSnapshot().percent).toBe(50)
|
expect(tracker.getSnapshot().percent).toBe(50)
|
||||||
|
|
||||||
const res2 = await globalThis.fetch('/ui/frontend/manifest.json')
|
const res2 = await globalThis.fetch('/ui/frontend/manifest.json')
|
||||||
expect(tracker.getSnapshot().currentFile).toBe('manifest.json')
|
expect(tracker.getSnapshot().currentFile).toBe('主菜单与大厅界面')
|
||||||
await res2.json()
|
await res2.json()
|
||||||
expect(tracker.getSnapshot().loadedCount).toBe(2)
|
expect(tracker.getSnapshot().loadedCount).toBe(2)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue