diff --git a/play.html b/play.html index f15f686..4c16fac 100644 --- a/play.html +++ b/play.html @@ -46,9 +46,146 @@ #d2-toolbar-container { flex: 0 0 auto; + width: 100%; z-index: 50; } + #d2-toolbar, + .d2-toolbar { + position: relative; + z-index: 1000; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + height: 32px; + min-height: 32px; + max-height: 32px; + padding: 0 12px; + box-sizing: border-box; + background: linear-gradient(180deg, #1b1712 0%, #0d0b08 100%); + border-bottom: 1px solid #4a3c24; + color: #d6c496; + font-family: var(--font-display); + font-size: 12px; + line-height: 1; + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + user-select: none; + } + + .d2-toolbar__group { + display: inline-flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + flex-shrink: 0; + gap: 6px; + white-space: nowrap; + } + + .d2-toolbar__status { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 8px; + border-radius: 3px; + background: #14110d; + border: 1px solid #352b1b; + } + + .d2-toolbar__status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + background: #6b6b6b; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.8); + } + + .d2-toolbar__status-dot--disconnected { background: #767676; } + .d2-toolbar__status-dot--connecting { background: #e6a822; box-shadow: 0 0 6px #e6a822; } + .d2-toolbar__status-dot--bncs { background: #4495e8; box-shadow: 0 0 6px #4495e8; } + .d2-toolbar__status-dot--mcp { background: #36c5d6; box-shadow: 0 0 6px #36c5d6; } + .d2-toolbar__status-dot--d2gs { background: #2ecc71; box-shadow: 0 0 6px #2ecc71; } + .d2-toolbar__status-dot--error { background: #e74c3c; box-shadow: 0 0 6px #e74c3c; } + + .d2-toolbar__rtt { + font-family: "JetBrains Mono", "Consolas", monospace; + font-size: 11px; + color: #a89974; + } + + .d2-toolbar__label { + color: #b59e68; + font-size: 11px; + letter-spacing: 0.03em; + } + + .d2-toolbar__select { + height: 22px; + padding: 0 6px; + background: #120f0b; + border: 1px solid #4a3c24; + border-radius: 2px; + color: #e2d1a0; + font-size: 11px; + cursor: pointer; + outline: none; + } + + .d2-toolbar__select:hover, + .d2-toolbar__btn:hover { + border-color: #c7b377; + color: #fff2c6; + } + + .d2-toolbar__btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 22px; + padding: 0 8px; + background: linear-gradient(180deg, #2b2316 0%, #17120b 100%); + border: 1px solid #574629; + border-radius: 2px; + color: #dfc991; + font-size: 11px; + cursor: pointer; + } + + .d2-toolbar__btn--active { + background: linear-gradient(180deg, #524020 0%, #2e2310 100%); + border-color: #d4b46a; + color: #fff5d6; + } + + .d2-toolbar__diag { + position: absolute; + top: 32px; + left: 12px; + right: 12px; + z-index: 1001; + display: none; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + background: rgba(38, 12, 10, 0.96); + border: 1px solid #a93226; + border-top: none; + border-radius: 0 0 4px 4px; + color: #ffd6d1; + font-size: 12px; + } + + .d2-toolbar__diag--visible { + display: flex; + } + #play-workspace { flex: 1 1 auto; min-height: 0; diff --git a/src/client/main.ts b/src/client/main.ts index 08cb3ed..69becb2 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -169,7 +169,39 @@ async function bootPlayApp(): Promise { const initialSettings = settingsStore.get() setLang(initialSettings.lang) + let sessionRef: OnlineSession | null = null + + const toolbar = new Toolbar({ + store: settingsStore, + container: toolbarContainer, + callbacks: { + onLangChange: (lang: GameLang) => { + setLang(lang) + sessionRef?.hudManager.setLang(lang) + }, + onViewportChange: (profile: ViewportProfile) => { + applyViewportProfileToDom(profile, { + stageEl, + glCanvas: viewCanvas, + hudCanvas, + }) + sessionRef?.hudManager.resize(profile.width, profile.height) + sessionRef?.camera.resize(profile) + }, + onAutomapRevealChange: (mode) => { + sessionRef?.automapView.setMode(mode) + }, + onInspectorToggle: (open) => { + sessionRef?.packetInspector.setOpen(open) + }, + onServerHostReconnect: (newHost) => { + void sessionRef?.disconnect(`Reconnecting to ${newHost}`) + }, + }, + }) + const initialCacheStatus = await registerAssetCache() + toolbar.setAssetCacheUsageBytes(initialCacheStatus.usageBytes) const minimap = new Minimap() const renderer = new SpriteRenderer(viewCanvas) @@ -183,8 +215,6 @@ async function bootPlayApp(): Promise { let itemsAtlasHandle: AtlasHandle | undefined let lastRenderSnapshot: SceneFrameSnapshot | null = null - let sessionRef: OnlineSession | null = null - const hudManager = new HudManager(hudCanvas, { onRenderAutomapOverlay: (ctx, font, logicalWidth, logicalHeight) => { minimap.drawHudOverlay(ctx, font, logicalWidth, logicalHeight) @@ -339,35 +369,6 @@ async function bootPlayApp(): Promise { } } - const toolbar = new Toolbar({ - store: settingsStore, - container: toolbarContainer, - callbacks: { - onLangChange: (lang: GameLang) => { - setLang(lang) - session.hudManager.setLang(lang) - }, - onViewportChange: (profile: ViewportProfile) => { - applyViewportProfileToDom(profile, { - stageEl, - glCanvas: viewCanvas, - hudCanvas, - }) - session.hudManager.resize(profile.width, profile.height) - session.camera.resize(profile) - }, - onAutomapRevealChange: (mode) => { - session.automapView.setMode(mode) - }, - onInspectorToggle: (open) => { - inspector.setOpen(open) - }, - onServerHostReconnect: (newHost) => { - void session.disconnect(`Reconnecting to ${newHost}`) - }, - }, - }) - toolbar.setAssetCacheUsageBytes(initialCacheStatus.usageBytes) void assetsReadyPromise.then(async () => { const updatedStatus = await getAssetCacheStatus() toolbar.setAssetCacheUsageBytes(updatedStatus.usageBytes) diff --git a/src/client/toolbar/toolbar.css b/src/client/toolbar/toolbar.css index 6acfa71..50b5cba 100644 --- a/src/client/toolbar/toolbar.css +++ b/src/client/toolbar/toolbar.css @@ -5,9 +5,11 @@ position: relative; z-index: 1000; display: flex; + flex-direction: row; + flex-wrap: nowrap; align-items: center; justify-content: space-between; - gap: 10px; + gap: 12px; width: 100%; height: 32px; min-height: 32px; @@ -17,15 +19,21 @@ background: linear-gradient(180deg, #1b1712 0%, #0d0b08 100%); border-bottom: 1px solid #4a3c24; color: #d6c496; - font-family: "Exocet", "Cinzel", "Noto Serif SC", "PingFang SC", Georgia, serif; + font-family: var(--font-display, "D2Exocet", "Cinzel", "Noto Serif SC", "PingFang SC", Georgia, serif); font-size: 12px; line-height: 1; + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; user-select: none; } .d2-toolbar__group { display: inline-flex; + flex-direction: row; + flex-wrap: nowrap; align-items: center; + flex-shrink: 0; gap: 6px; white-space: nowrap; } diff --git a/src/client/toolbar/toolbar.ts b/src/client/toolbar/toolbar.ts index 2ca7b32..666eccc 100644 --- a/src/client/toolbar/toolbar.ts +++ b/src/client/toolbar/toolbar.ts @@ -82,13 +82,12 @@ export interface ToolbarSnapshot { } /** - * Persistent 32px Top DOM Toolbar (`#d2-toolbar`) with 6 primary controls: + * Persistent 32px Top DOM Toolbar (`#d2-toolbar`) with 5 primary controls: * 1. Connection Status + RTT (`ms`) + Diagnostics (5-per-IP quota / WSS TLS error) + Inspector toggle - * 2. Server Host Input (`www.laiseek.xyz` default, bare-IP over HTTPS guard, mid-game reconnect prompt) - * 3. Live Language Toggle (`中` / `EN`) - * 4. Lighting Preset Selector (`auto | noon | dusk | night | torch | cold | fullbright`) - * 5. Automap Reveal Selector (`off | level | act`) - * 6. Viewport Size Toggle (`800x600 | 1024x768`) + Asset Cache Clear button + * 2. Live Language Toggle (`中` / `EN`) + * 3. Lighting Preset Selector (`auto | noon | dusk | night | torch | cold | fullbright`) + * 4. Automap Reveal Selector (`off | level | act`) + * 5. Viewport Size Toggle (`800x600 | 1024x768`) + Asset Cache Clear button */ export class Toolbar { private readonly store: SettingsStore @@ -112,9 +111,6 @@ export class Toolbar { private statusDotEl: HTMLElement | null = null private statusTextEl: HTMLElement | null = null private rttTextEl: HTMLElement | null = null - private serverLabelEl: HTMLElement | null = null - private serverInputEl: HTMLInputElement | null = null - private serverApplyBtnEl: HTMLButtonElement | null = null private langBtnZhEl: HTMLButtonElement | null = null private langBtnEnEl: HTMLButtonElement | null = null private lightingLabelEl: HTMLElement | null = null @@ -395,8 +391,146 @@ export class Toolbar { this.rootEl = null } + private ensureStyles(doc: Document): void { + if (!doc.head || doc.getElementById('d2-toolbar-styles')) return + const styleEl = doc.createElement('style') + styleEl.id = 'd2-toolbar-styles' + styleEl.textContent = ` +#d2-toolbar, +.d2-toolbar { + position: relative; + z-index: 1000; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + height: 32px; + min-height: 32px; + max-height: 32px; + padding: 0 12px; + box-sizing: border-box; + background: linear-gradient(180deg, #1b1712 0%, #0d0b08 100%); + border-bottom: 1px solid #4a3c24; + color: #d6c496; + font-family: var(--font-display, "D2Exocet", "Cinzel", "Noto Serif SC", "PingFang SC", Georgia, serif); + font-size: 12px; + line-height: 1; + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + user-select: none; +} +.d2-toolbar__group { + display: inline-flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + flex-shrink: 0; + gap: 6px; + white-space: nowrap; +} +.d2-toolbar__divider { + width: 1px; + height: 18px; + background: #3a2f1d; +} +.d2-toolbar__status { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 8px; + border-radius: 3px; + background: #14110d; + border: 1px solid #352b1b; +} +.d2-toolbar__status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + background: #6b6b6b; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.8); +} +.d2-toolbar__status-dot--disconnected { background: #767676; } +.d2-toolbar__status-dot--connecting { background: #e6a822; box-shadow: 0 0 6px #e6a822; } +.d2-toolbar__status-dot--bncs { background: #4495e8; box-shadow: 0 0 6px #4495e8; } +.d2-toolbar__status-dot--mcp { background: #36c5d6; box-shadow: 0 0 6px #36c5d6; } +.d2-toolbar__status-dot--d2gs { background: #2ecc71; box-shadow: 0 0 6px #2ecc71; } +.d2-toolbar__status-dot--error { background: #e74c3c; box-shadow: 0 0 6px #e74c3c; } +.d2-toolbar__rtt { + font-family: "JetBrains Mono", "Consolas", monospace; + font-size: 11px; + color: #a89974; +} +.d2-toolbar__label { + color: #b59e68; + font-size: 11px; + letter-spacing: 0.03em; +} +.d2-toolbar__select { + height: 22px; + padding: 0 6px; + background: #120f0b; + border: 1px solid #4a3c24; + border-radius: 2px; + color: #e2d1a0; + font-size: 11px; + cursor: pointer; + outline: none; +} +.d2-toolbar__select:hover, +.d2-toolbar__btn:hover { + border-color: #c7b377; + color: #fff2c6; +} +.d2-toolbar__btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 22px; + padding: 0 8px; + background: linear-gradient(180deg, #2b2316 0%, #17120b 100%); + border: 1px solid #574629; + border-radius: 2px; + color: #dfc991; + font-size: 11px; + cursor: pointer; +} +.d2-toolbar__btn--active { + background: linear-gradient(180deg, #524020 0%, #2e2310 100%); + border-color: #d4b46a; + color: #fff5d6; +} +.d2-toolbar__diag { + position: absolute; + top: 32px; + left: 12px; + right: 12px; + z-index: 1001; + display: none; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + background: rgba(38, 12, 10, 0.96); + border: 1px solid #a93226; + border-top: none; + border-radius: 0 0 4px 4px; + color: #ffd6d1; + font-size: 12px; +} +.d2-toolbar__diag--visible { + display: flex; +} +` + doc.head.appendChild(styleEl) + } + private mountDom(container: HTMLElement | null): void { const doc = document + this.ensureStyles(doc) const root = doc.createElement('header') root.id = 'd2-toolbar' root.className = 'd2-toolbar' @@ -430,35 +564,7 @@ export class Toolbar { statusGroup.appendChild(statusBadge) statusGroup.appendChild(inspectorBtn) - // Group 2: Server Host Input - const serverGroup = doc.createElement('div') - serverGroup.className = 'd2-toolbar__group' - const serverLabel = doc.createElement('label') - serverLabel.className = 'd2-toolbar__label' - serverLabel.htmlFor = 'd2-toolbar-server-host' - const serverInput = doc.createElement('input') - serverInput.type = 'text' - serverInput.id = 'd2-toolbar-server-host' - serverInput.className = 'd2-toolbar__input' - serverInput.value = this.serverInputValue - serverInput.addEventListener('keydown', (ev) => { - if (ev.key === 'Enter') { - ev.preventDefault() - this.submitServerHost(serverInput.value) - } - }) - const serverApplyBtn = doc.createElement('button') - serverApplyBtn.type = 'button' - serverApplyBtn.id = 'd2-toolbar-server-apply' - serverApplyBtn.className = 'd2-toolbar__btn' - serverApplyBtn.addEventListener('click', () => { - this.submitServerHost(serverInput.value) - }) - serverGroup.appendChild(serverLabel) - serverGroup.appendChild(serverInput) - serverGroup.appendChild(serverApplyBtn) - - // Group 3: Language Toggle (中 / EN) + // Group 2: Language Toggle (中 / EN) const langGroup = doc.createElement('div') langGroup.className = 'd2-toolbar__group' const langBtnZh = doc.createElement('button') @@ -478,7 +584,7 @@ export class Toolbar { langGroup.appendChild(langBtnZh) langGroup.appendChild(langBtnEn) - // Group 4: Lighting Selector + // Group 3: Lighting Selector const lightingGroup = doc.createElement('div') lightingGroup.className = 'd2-toolbar__group' const lightingLabel = doc.createElement('label') @@ -493,7 +599,7 @@ export class Toolbar { lightingGroup.appendChild(lightingLabel) lightingGroup.appendChild(lightingSelect) - // Group 5: Automap Reveal Selector (Off / Level / Act) + // Group 4: Automap Reveal Selector (Off / Level / Act) const automapGroup = doc.createElement('div') automapGroup.className = 'd2-toolbar__group' const automapLabel = doc.createElement('label') @@ -508,7 +614,7 @@ export class Toolbar { automapGroup.appendChild(automapLabel) automapGroup.appendChild(automapSelect) - // Group 6: Viewport Size Selector (800x600 / 1024x768) + Asset Cache Clear button + // Group 5: Viewport Size Selector (800x600 / 1024x768) + Asset Cache Clear button const viewportGroup = doc.createElement('div') viewportGroup.className = 'd2-toolbar__group' const viewportLabel = doc.createElement('label') @@ -545,7 +651,6 @@ export class Toolbar { diagBanner.className = 'd2-toolbar__diag' root.appendChild(statusGroup) - root.appendChild(serverGroup) root.appendChild(langGroup) root.appendChild(lightingGroup) root.appendChild(automapGroup) @@ -560,9 +665,6 @@ export class Toolbar { this.statusDotEl = dot this.statusTextEl = statusText this.rttTextEl = rttText - this.serverLabelEl = serverLabel - this.serverInputEl = serverInput - this.serverApplyBtnEl = serverApplyBtn this.langBtnZhEl = langBtnZh this.langBtnEnEl = langBtnEn this.lightingLabelEl = lightingLabel @@ -598,21 +700,6 @@ export class Toolbar { ? 'd2-toolbar__btn d2-toolbar__btn--active' : 'd2-toolbar__btn' } - if (this.serverLabelEl) { - this.serverLabelEl.textContent = tToolbar('toolbar.server.label', lang) - } - if (this.serverInputEl) { - this.serverInputEl.placeholder = tToolbar('toolbar.server.placeholder', lang) - if (this.serverInputEl.value !== snap.serverInputValue) { - this.serverInputEl.value = snap.serverInputValue - } - this.serverInputEl.className = snap.serverInputValid - ? 'd2-toolbar__input' - : 'd2-toolbar__input d2-toolbar__input--invalid' - } - if (this.serverApplyBtnEl) { - this.serverApplyBtnEl.textContent = tToolbar('toolbar.server.apply', lang) - } if (this.langBtnZhEl) { this.langBtnZhEl.className = lang === 'zh' ? 'd2-toolbar__btn d2-toolbar__btn--active' : 'd2-toolbar__btn'