|
4 | 4 | <meta charset="utf-8"> |
5 | 5 | <title>Deep Future</title> |
6 | 6 | <style> |
7 | | - body { margin: 0; background: #000; } |
8 | | - canvas { display: block; width: 100vw; height: 100vh; } |
| 7 | + body { |
| 8 | + margin: 0; |
| 9 | + background: #000; |
| 10 | + font-family: "Segoe UI", "PingFang SC", "Hiragino Sans GB", Arial, sans-serif; |
| 11 | + color: #f5f5f5; |
| 12 | + } |
| 13 | + canvas { |
| 14 | + display: block; |
| 15 | + width: 100vw; |
| 16 | + height: 100vh; |
| 17 | + } |
| 18 | + #overlay { |
| 19 | + position: fixed; |
| 20 | + inset: 0; |
| 21 | + display: flex; |
| 22 | + align-items: center; |
| 23 | + justify-content: center; |
| 24 | + padding: 24px; |
| 25 | + box-sizing: border-box; |
| 26 | + background: radial-gradient(circle at top, rgba(36, 64, 128, 0.8), rgba(6, 10, 24, 0.95)); |
| 27 | + text-align: center; |
| 28 | + transition: opacity 0.4s ease; |
| 29 | + z-index: 10; |
| 30 | + } |
| 31 | + #overlay.hidden { |
| 32 | + opacity: 0; |
| 33 | + pointer-events: none; |
| 34 | + } |
| 35 | + #overlay.error { |
| 36 | + background: rgba(16, 16, 16, 0.95); |
| 37 | + } |
| 38 | + .overlay-content { |
| 39 | + max-width: 640px; |
| 40 | + } |
| 41 | + #overlay-title { |
| 42 | + font-size: 2rem; |
| 43 | + letter-spacing: 0.08em; |
| 44 | + text-transform: uppercase; |
| 45 | + margin: 0 0 12px; |
| 46 | + } |
| 47 | + #overlay-intro { |
| 48 | + font-size: 1rem; |
| 49 | + line-height: 1.6; |
| 50 | + margin-bottom: 24px; |
| 51 | + } |
| 52 | + #overlay-intro p { |
| 53 | + margin: 0 0 12px; |
| 54 | + } |
| 55 | + #overlay-status { |
| 56 | + font-size: 0.95rem; |
| 57 | + opacity: 0.85; |
| 58 | + margin: 0; |
| 59 | + } |
9 | 60 | </style> |
10 | 61 | <script src="./coi-serviceworker.min.js"></script> |
11 | 62 | </head> |
12 | 63 | <body> |
| 64 | + <div id="overlay"> |
| 65 | + <div class="overlay-content"> |
| 66 | + <h1 id="overlay-title"></h1> |
| 67 | + <div id="overlay-intro"></div> |
| 68 | + <p id="overlay-status"></p> |
| 69 | + </div> |
| 70 | + </div> |
13 | 71 | <canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas> |
14 | 72 |
|
15 | 73 | <script> |
16 | | - var Module = { |
17 | | - arguments: ["zipfile=/data/main.zip:/data/font.zip"], |
18 | | - canvas: document.getElementById('canvas'), |
19 | | - preRun: [function () { |
20 | | - const runDependency = 'load-main-zip'; |
21 | | - const fontDependency = 'load-font'; |
22 | | - |
23 | | - const originalLookup = MEMFS.lookup; |
24 | | - MEMFS.lookup = function (parent, name) { |
25 | | - try { |
26 | | - return originalLookup.call(this, parent, name); |
27 | | - } catch (e) { |
28 | | - console.error('[MEMFS lookup failed]', |
29 | | - 'parent:', Module.FS.getPath(parent), |
30 | | - 'name:', name, e); |
31 | | - throw e; |
32 | | - } |
33 | | - }; |
| 74 | + (function () { |
| 75 | + const overlay = document.getElementById('overlay'); |
| 76 | + const titleEl = document.getElementById('overlay-title'); |
| 77 | + const introEl = document.getElementById('overlay-intro'); |
| 78 | + const statusEl = document.getElementById('overlay-status'); |
| 79 | + |
| 80 | + const locale = (navigator.language || navigator.userLanguage || 'en').toLowerCase(); |
| 81 | + const isChinese = locale.startsWith('zh'); |
| 82 | + |
| 83 | + const strings = isChinese ? { |
| 84 | + gameTitle: '深远未来', |
| 85 | + intro: [ |
| 86 | + '拓殖星球,发展科技,永无止境地进化你银河系的深远未来。', |
| 87 | + '《深远未来》是桌游的数字化版本,带来“边玩边创造”的独特体验,每一局都能拓展属于你的银河文明。' |
| 88 | + ], |
| 89 | + checkingWebGPU: '正在检查 WebGPU 支持…', |
| 90 | + webgpuMissingTitle: '当前浏览器未开启 WebGPU 支持', |
| 91 | + webgpuMissingDetail: '请在支持 WebGPU 的浏览器中打开,或者在浏览器设置中启用 WebGPU 后刷新页面。', |
| 92 | + loadingResources: '正在加载游戏资源…', |
| 93 | + loadingMainArchive: '正在加载核心资源包…', |
| 94 | + loadingFontArchive: '正在加载字体资源包…', |
| 95 | + runtimeReady: '引擎已就绪,正在启动…', |
| 96 | + runtimeFailedTitle: '运行时加载失败', |
| 97 | + runtimeFailedDetail: '加载运行时失败,请稍后重试。' |
| 98 | + } : { |
| 99 | + gameTitle: 'Deep Future', |
| 100 | + intro: [ |
| 101 | + 'Settle worlds, advance techs, and endlessly evolve your galaxy\'s deep future.', |
| 102 | + 'Deep Future is the digital take on the make-as-you-play board game where every session grows your galactic civilization.' |
| 103 | + ], |
| 104 | + checkingWebGPU: 'Checking for WebGPU support…', |
| 105 | + webgpuMissingTitle: 'WebGPU is not enabled in this browser', |
| 106 | + webgpuMissingDetail: 'Open the page in a browser with WebGPU support or enable it in your browser settings, then reload.', |
| 107 | + loadingResources: 'Loading game assets…', |
| 108 | + loadingMainArchive: 'Fetching core content…', |
| 109 | + loadingFontArchive: 'Fetching font resources…', |
| 110 | + runtimeReady: 'Runtime ready. Launching…', |
| 111 | + runtimeFailedTitle: 'Runtime failed to load', |
| 112 | + runtimeFailedDetail: 'We could not load the runtime. Please try again later.' |
| 113 | + }; |
| 114 | + |
| 115 | + titleEl.textContent = strings.gameTitle; |
| 116 | + introEl.innerHTML = strings.intro.map(text => '<p>' + text + '</p>').join(''); |
| 117 | + |
| 118 | + function setStatus(message) { |
| 119 | + statusEl.textContent = message; |
| 120 | + } |
| 121 | + |
| 122 | + function showError(title, detail) { |
| 123 | + overlay.classList.remove('hidden'); |
| 124 | + overlay.classList.add('error'); |
| 125 | + titleEl.textContent = title; |
| 126 | + setStatus(detail); |
| 127 | + } |
34 | 128 |
|
35 | | - Module.FS_createPath('/', 'data', true, true); |
| 129 | + function hideOverlay() { |
| 130 | + overlay.classList.add('hidden'); |
| 131 | + } |
36 | 132 |
|
| 133 | + async function hasWebGPU() { |
| 134 | + if (!navigator.gpu || typeof navigator.gpu.requestAdapter !== 'function') { |
| 135 | + return false; |
| 136 | + } |
37 | 137 | try { |
38 | | - FS.mkdir('/persistent'); |
39 | | - FS.mount(IDBFS, {autoPersist: true}, '/persistent'); |
40 | | - FS.syncfs(true, err => { |
41 | | - if (err) console.error('Failed to sync from IDBFS', err); |
42 | | - else console.log('Synced from IDBFS'); |
43 | | - }); |
44 | | - |
45 | | - setInterval(() => { |
46 | | - FS.syncfs(false, err => { |
47 | | - if (err) console.error('Failed to sync to IDBFS', err); |
48 | | - else console.log('Synced to IDBFS'); |
| 138 | + const adapter = await navigator.gpu.requestAdapter(); |
| 139 | + return Boolean(adapter); |
| 140 | + } catch (err) { |
| 141 | + console.error('WebGPU adapter request failed', err); |
| 142 | + return false; |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + function loadRuntimeScript() { |
| 147 | + return new Promise(function (resolve, reject) { |
| 148 | + const script = document.createElement('script'); |
| 149 | + script.src = './soluna.js'; |
| 150 | + script.onload = () => resolve(); |
| 151 | + script.onerror = () => reject(new Error('Failed to load soluna.js')); |
| 152 | + document.head.appendChild(script); |
| 153 | + }); |
| 154 | + } |
| 155 | + |
| 156 | + setStatus(strings.checkingWebGPU); |
| 157 | + |
| 158 | + hasWebGPU().then(function (supported) { |
| 159 | + if (!supported) { |
| 160 | + showError(strings.webgpuMissingTitle, strings.webgpuMissingDetail); |
| 161 | + return; |
| 162 | + } |
| 163 | + |
| 164 | + setStatus(strings.loadingResources); |
| 165 | + |
| 166 | + window.Module = { |
| 167 | + arguments: ["zipfile=/data/main.zip:/data/font.zip"], |
| 168 | + canvas: document.getElementById('canvas'), |
| 169 | + preRun: [function () { |
| 170 | + const runDependency = 'load-main-zip'; |
| 171 | + const fontDependency = 'load-font'; |
| 172 | + |
| 173 | + const originalLookup = MEMFS.lookup; |
| 174 | + MEMFS.lookup = function (parent, name) { |
| 175 | + try { |
| 176 | + return originalLookup.call(this, parent, name); |
| 177 | + } catch (e) { |
| 178 | + console.error('[MEMFS lookup failed]', |
| 179 | + 'parent:', Module.FS.getPath(parent), |
| 180 | + 'name:', name, e); |
| 181 | + throw e; |
| 182 | + } |
| 183 | + }; |
| 184 | + |
| 185 | + Module.FS_createPath('/', 'data', true, true); |
| 186 | + |
| 187 | + try { |
| 188 | + FS.mkdir('/persistent'); |
| 189 | + FS.mount(IDBFS, { autoPersist: true }, '/persistent'); |
| 190 | + FS.syncfs(true, err => { |
| 191 | + if (err) console.error('Failed to sync from IDBFS', err); |
| 192 | + else console.log('Synced from IDBFS'); |
49 | 193 | }); |
50 | | - }, 10000); |
51 | | - } catch (e) {} |
52 | | - |
53 | | - Module.addRunDependency(runDependency); |
54 | | - fetch('./main.zip') |
55 | | - .then(function (response) { |
56 | | - if (!response.ok) { |
57 | | - throw new Error('HTTP ' + response.status + ' while fetching main.zip'); |
58 | | - } |
59 | | - return response.arrayBuffer(); |
60 | | - }) |
61 | | - .then(function (buffer) { |
62 | | - const data = new Uint8Array(buffer); |
63 | | - Module.FS.writeFile('/data/main.zip', data, { canOwn: true }); |
64 | | - console.log('main.zip loaded:', Module.FS.readdir('/data')); |
65 | | - }) |
66 | | - .catch(function (err) { |
67 | | - console.error('Failed to load main.zip', err); |
68 | | - throw err; |
69 | | - }) |
70 | | - .finally(function () { |
71 | | - Module.removeRunDependency(runDependency); |
72 | | - }); |
73 | | - Module.addRunDependency(fontDependency); |
74 | | - fetch('./font.zip') |
75 | | - .then(function (response) { |
76 | | - if (!response.ok) { |
77 | | - throw new Error('HTTP ' + response.status + ' while fetching font.zip'); |
| 194 | + |
| 195 | + setInterval(() => { |
| 196 | + FS.syncfs(false, err => { |
| 197 | + if (err) console.error('Failed to sync to IDBFS', err); |
| 198 | + else console.log('Synced to IDBFS'); |
| 199 | + }); |
| 200 | + }, 10000); |
| 201 | + } catch (e) { |
| 202 | + console.warn('Failed to init persistent storage', e); |
78 | 203 | } |
79 | | - return response.arrayBuffer(); |
80 | | - }) |
81 | | - .then(function (buffer) { |
82 | | - const data = new Uint8Array(buffer); |
83 | | - Module.FS.writeFile('/data/font.zip', data, { canOwn: true }); |
84 | | - console.log('font.zip loaded:', Module.FS.readdir('/data')); |
85 | | - }) |
86 | | - .catch(function (err) { |
87 | | - console.error('Failed to load font.zip', err); |
88 | | - throw err; |
89 | | - }) |
90 | | - .finally(function () { |
91 | | - Module.removeRunDependency(fontDependency); |
92 | | - }); |
93 | | - }], |
94 | | - onRuntimeInitialized: function () { |
95 | | - console.log('Soluna runtime ready'); |
96 | | - }, |
97 | | - onExit: function (status) { |
98 | | - console.log('Program exited with status', status); |
99 | | - }, |
100 | | - onAbort: function (what) { |
101 | | - console.error('Program aborted:', what); |
102 | | - }, |
103 | | - print: console.log, |
104 | | - printErr: console.error |
105 | | - }; |
| 204 | + |
| 205 | + Module.addRunDependency(runDependency); |
| 206 | + setStatus(strings.loadingMainArchive); |
| 207 | + fetch('./main.zip') |
| 208 | + .then(function (response) { |
| 209 | + if (!response.ok) { |
| 210 | + throw new Error('HTTP ' + response.status + ' while fetching main.zip'); |
| 211 | + } |
| 212 | + return response.arrayBuffer(); |
| 213 | + }) |
| 214 | + .then(function (buffer) { |
| 215 | + const data = new Uint8Array(buffer); |
| 216 | + Module.FS.writeFile('/data/main.zip', data, { canOwn: true }); |
| 217 | + console.log('main.zip loaded:', Module.FS.readdir('/data')); |
| 218 | + }) |
| 219 | + .catch(function (err) { |
| 220 | + console.error('Failed to load main.zip', err); |
| 221 | + throw err; |
| 222 | + }) |
| 223 | + .finally(function () { |
| 224 | + Module.removeRunDependency(runDependency); |
| 225 | + }); |
| 226 | + Module.addRunDependency(fontDependency); |
| 227 | + setStatus(strings.loadingFontArchive); |
| 228 | + fetch('./font.zip') |
| 229 | + .then(function (response) { |
| 230 | + if (!response.ok) { |
| 231 | + throw new Error('HTTP ' + response.status + ' while fetching font.zip'); |
| 232 | + } |
| 233 | + return response.arrayBuffer(); |
| 234 | + }) |
| 235 | + .then(function (buffer) { |
| 236 | + const data = new Uint8Array(buffer); |
| 237 | + Module.FS.writeFile('/data/font.zip', data, { canOwn: true }); |
| 238 | + console.log('font.zip loaded:', Module.FS.readdir('/data')); |
| 239 | + }) |
| 240 | + .catch(function (err) { |
| 241 | + console.error('Failed to load font.zip', err); |
| 242 | + throw err; |
| 243 | + }) |
| 244 | + .finally(function () { |
| 245 | + Module.removeRunDependency(fontDependency); |
| 246 | + }); |
| 247 | + }], |
| 248 | + onRuntimeInitialized: function () { |
| 249 | + console.log('Soluna runtime ready'); |
| 250 | + setStatus(strings.runtimeReady); |
| 251 | + setTimeout(hideOverlay, 400); |
| 252 | + }, |
| 253 | + onExit: function (status) { |
| 254 | + console.log('Program exited with status', status); |
| 255 | + }, |
| 256 | + onAbort: function (what) { |
| 257 | + console.error('Program aborted:', what); |
| 258 | + showError(strings.runtimeFailedTitle, strings.runtimeFailedDetail); |
| 259 | + }, |
| 260 | + print: console.log, |
| 261 | + printErr: console.error |
| 262 | + }; |
| 263 | + |
| 264 | + loadRuntimeScript().catch(function (err) { |
| 265 | + console.error(err); |
| 266 | + showError(strings.runtimeFailedTitle, strings.runtimeFailedDetail); |
| 267 | + }); |
| 268 | + }); |
| 269 | + })(); |
106 | 270 | </script> |
107 | | - <script src="./soluna.js"></script> |
108 | 271 | </body> |
109 | 272 | </html> |
0 commit comments