From df28be81f6050de2727a45ddba305134bf7af15e Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 25 Oct 2026 20:50:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=9C=AC=E5=9C=B0=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E4=BB=A3=E7=90=86=20+=20=E8=BD=A6=E5=BA=93=E5=85=A8?= =?UTF-8?q?=E5=B1=8F=E5=B8=83=E5=B1=80=E4=B8=8E=E9=80=80=E5=87=BA=E5=85=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/App/Browser/Ui/GarageScreen.kt | 9 + .../app/src/jsMain/resources/Garage3D.js | 24 +- .../app/src/jsMain/resources/Garage3D2.js | 168 ++++++ .../app/src/jsMain/resources/index.html | 6 +- .../app/src/jsMain/resources/lobby.css | 33 + .../app/src/jsMain/resources/lobby2.css | 564 ++++++++++++++++++ TankiOffline/tools/res_proxy.js | 145 +++++ TankiOffline/webpack.config.d/res-proxy.js | 10 + 8 files changed, 944 insertions(+), 15 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/resources/Garage3D2.js create mode 100644 TankiOffline/app/src/jsMain/resources/lobby2.css create mode 100644 TankiOffline/tools/res_proxy.js create mode 100644 TankiOffline/webpack.config.d/res-proxy.js diff --git a/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/GarageScreen.kt b/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/GarageScreen.kt index 69fd019..2fce16a 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/GarageScreen.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/GarageScreen.kt @@ -78,6 +78,7 @@ private val GarageScreen = FC { header { Css("g2-head") + style = js("({ position: 'relative', zIndex: 5 })") button { Css("g2-back") onClick = { GarageContext.OnBack() } @@ -89,6 +90,11 @@ private val GarageScreen = FC { span { img { src = Asset("static/images/ruby.ce62efbc.svg"); Css("coin") }; +"${GarageContext.Rubies}" } span { img { src = Asset("static/images/crystalSmall.242e6c15.svg"); Css("coin") }; +"${GarageContext.Crystals}" } } + button { + Css("g2-exit") + onClick = { js("window.location.reload()") } + i { Css("g2-exit-icon") } + } } div { @@ -96,6 +102,7 @@ private val GarageScreen = FC { aside { Css("g2-left") + style = js("({ zIndex: 3 })") div { Css("g2-gs") img { src = Asset("static/images/iconGSSmall.0c0aa8fa.svg") } @@ -131,6 +138,7 @@ private val GarageScreen = FC { aside { Css("g2-right") + style = js("({ zIndex: 3 })") MountedBlock("无人机", Drone) MountedBlock("手榴弹", Grenade) MountedBlock("炮塔", Turret) @@ -140,6 +148,7 @@ private val GarageScreen = FC { div { Css("g2-supplies") + style = js("({ zIndex: 3 })") Supply("Repair.13e5e240.svg", "维修") Supply("armor.b0b6d887.svg", "护甲") Supply("damage.af4f0289.svg", "伤害") diff --git a/TankiOffline/app/src/jsMain/resources/Garage3D.js b/TankiOffline/app/src/jsMain/resources/Garage3D.js index a0f7d7b..41625d1 100644 --- a/TankiOffline/app/src/jsMain/resources/Garage3D.js +++ b/TankiOffline/app/src/jsMain/resources/Garage3D.js @@ -4,7 +4,7 @@ import * as THREE from 'three'; import { TDSLoader } from '/vendor/three/TDSLoader.js'; import { OrbitControls } from '/vendor/three/OrbitControls.js'; -const MapUrl = 'https://res.3dtank.com/613/2501/252/46/32047724143075/object.3ds'; +const MapUrl = 'http://127.0.0.1:8082/res/613/2501/252/46/32047724143075/object.3ds'; let Current = null; function PlaceholderTank() { @@ -42,21 +42,22 @@ function Mount(CanvasId) { } Mount.Retry = 0; - const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); + const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false, preserveDrawingBuffer: true }); + renderer.setClearColor(0x080f16, 1); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.shadowMap.enabled = true; const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100); - camera.position.set(4.2, 2.6, 5.2); + const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 200); + camera.position.set(7, 4.5, 9); const controls = new OrbitControls(camera, canvas); - controls.target.set(0, 1.0, 0); + controls.target.set(0, 2.0, 0); controls.enablePan = false; controls.enableZoom = true; - controls.minDistance = 3; - controls.maxDistance = 12; + controls.minDistance = 5; + controls.maxDistance = 26; controls.autoRotate = true; controls.autoRotateSpeed = 0.7; @@ -87,11 +88,10 @@ function Mount(CanvasId) { Platform.castShadow = true; scene.add(Platform); - const Tank = PlaceholderTank(); - Tank.position.y = 0.18; - scene.add(Tank); + // 真实坦克模型待 Item3DCC.Resource 索引接入;先不放几何占位,避免误导。 - new TDSLoader().load( + const ShowMap = false; + if (ShowMap) new TDSLoader().load( MapUrl, (Obj) => { Obj.traverse((N) => { @@ -112,7 +112,7 @@ function Mount(CanvasId) { const Box = new THREE.Box3().setFromObject(Obj); const Size = Box.getSize(new THREE.Vector3()); const Center = Box.getCenter(new THREE.Vector3()); - const Scale = 6.5 / Math.max(Size.x, Size.y, Size.z); + const Scale = 11 / Math.max(Size.x, Size.y, Size.z); Obj.scale.setScalar(Scale); Obj.position.set(-Center.x * Scale, -Box.min.y * Scale, -Center.z * Scale); scene.add(Obj); diff --git a/TankiOffline/app/src/jsMain/resources/Garage3D2.js b/TankiOffline/app/src/jsMain/resources/Garage3D2.js new file mode 100644 index 0000000..41625d1 --- /dev/null +++ b/TankiOffline/app/src/jsMain/resources/Garage3D2.js @@ -0,0 +1,168 @@ +// Garage3D.js —— 车库 3D(Three.js 加载官方 3DS 场景 + 占位坦克) +// 由 index.html 以 - +
diff --git a/TankiOffline/app/src/jsMain/resources/lobby.css b/TankiOffline/app/src/jsMain/resources/lobby.css index 17cf84c..d06a1f7 100644 --- a/TankiOffline/app/src/jsMain/resources/lobby.css +++ b/TankiOffline/app/src/jsMain/resources/lobby.css @@ -524,3 +524,36 @@ body { margin: 0; overflow: hidden; background: #001926; color: #fff; } .group-icon.g1 { transform: translateX(-0.5em); } .group-icon.g2 { transform: translateX(0.5em); } .premium-block + .group-block + .rank-icon { border-left: 1px solid rgba(255, 255, 255, 0.25); } + +/* 车库全屏:3D 覆盖整页,左右参数/配件与底部道具作为浮层,header 保留 */ +.g2 { position: relative; } +.g2-body { + position: absolute; inset: 6em 0 0 0; display: block; padding: 0; gap: 0; +} +.g2-scene { + position: absolute; inset: 0; border: 0; border-radius: 0; background: #080f16; +} +.g2-left { + position: absolute; left: 2em; top: 2em; width: 22em; max-height: calc(100% - 4em); + overflow: auto; padding: 1em; box-sizing: border-box; + background: rgba(4, 14, 22, 0.82); border: 1px solid rgba(255, 255, 255, 0.10); + border-radius: 0.5em; backdrop-filter: blur(6px); +} +.g2-right { + position: absolute; right: 2em; top: 2em; width: 23em; padding: 1em; + box-sizing: border-box; background: rgba(4, 14, 22, 0.82); + border: 1px solid rgba(255, 255, 255, 0.10); border-radius: 0.5em; + backdrop-filter: blur(6px); +} +.g2-supplies { + position: absolute; left: 0; right: 0; bottom: 0; height: 9.6em; + background: rgba(3, 10, 16, 0.82); backdrop-filter: blur(6px); +} +.g2-scene-caption { bottom: 10.5em; pointer-events: none; } +.g2-resists { margin-top: 1em; } + +.g2-head { position: relative; z-index: 5; } +.g2-scene { z-index: 0; } +.g2-left, .g2-right { z-index: 3; } +.g2-supplies { z-index: 3; } +.g2-drawer-toggle { z-index: 4; } diff --git a/TankiOffline/app/src/jsMain/resources/lobby2.css b/TankiOffline/app/src/jsMain/resources/lobby2.css new file mode 100644 index 0000000..93d490b --- /dev/null +++ b/TankiOffline/app/src/jsMain/resources/lobby2.css @@ -0,0 +1,564 @@ +/* lobby.css —— 大厅主界面(尺寸/颜色对照 3dtank.com/play/ 线上计算样式) + + 线上根字号 12.96px(.ksc-37),这里用同样的 max(min(1.48148vh, 1vw), 3px), + 因此 6em = 78px、21.875em = 284px、22.5em = 292px,与真实 DOM 一致。 */ +@font-face { + font-family: BaseFontRegular; + src: url("/play/static/fonts/Rubik-Regular.7e001ec0.woff") format("woff"); +} +@font-face { + font-family: BaseFontMedium; + src: url("/play/static/fonts/Rubik-Medium.fc871fe9.woff") format("woff"); +} +@font-face { + font-family: BaseFontBold; + src: url("/play/static/fonts/Rubik-Bold.26ae1fce.woff") format("woff"); +} + +html, body, #app { height: 100%; } +body { margin: 0; overflow: hidden; background: #001926; color: #fff; } + +.ms-root { + position: relative; + width: 100%; + height: 100%; + font-size: max(min(1.48148vh, 1vw), 3px); + font-family: BaseFontRegular, "Segoe UI", sans-serif; + color: #fff; + user-select: none; + overflow: hidden; +} +#lobby-canvas { + position: absolute; inset: 0; width: 100%; height: 100%; + display: block; background: #001926; +} +.ms-bg { + position: absolute; inset: 0; + background: + linear-gradient(180deg, rgba(0, 25, 38, 0.35), rgba(0, 12, 20, 0.55)), + url("/play/static/images/background.fc2779c1.webp") center / cover no-repeat; + filter: saturate(0.9) brightness(0.8); +} + +/* ---------------- 顶栏 6em ---------------- */ +.ms-top { + position: absolute; top: 0; left: 0; + display: flex; align-items: center; justify-content: space-between; + width: 100%; height: 6em; z-index: 2; + border-bottom: 1px solid rgba(255, 255, 255, 0.25); + background: linear-gradient(180deg, rgba(0, 12, 20, 0.55), rgba(0, 12, 20, 0.05)); +} +.ms-top-left { display: flex; align-items: center; height: 100%; } +.rank-icon { + width: 5.9375em; height: 6em; display: flex; align-items: center; justify-content: center; + border-right: 1px solid rgba(255, 255, 255, 0.25); cursor: pointer; +} +.rank-icon:hover { background-color: rgba(255, 255, 255, 0.05); } +.rank-icon img { width: 4.375em; height: 4.375em; } +.user-progress { + display: flex; flex-direction: column; justify-content: center; + min-width: 10em; margin-left: 1em; height: 3.75em; +} +.user-name { + font: 500 1.1em/1.2 BaseFontMedium, sans-serif; + text-shadow: 0 0 0.25em rgba(0, 0, 0, 0.5); cursor: pointer; +} +.user-name:hover { color: #ffbc09; } +.xp-bar { + width: 12.5em; height: 0.25em; margin-top: 0.375em; + background: rgba(118, 255, 51, 0.25); border-radius: 0.375em; overflow: hidden; +} +.xp-bar i { display: block; height: 100%; background: rgba(118, 255, 51, 0.8); border-radius: inherit; } +.xp-text { + display: flex; align-items: center; margin-top: 0.35em; + font: 500 0.9375em/1 BaseFontMedium, sans-serif; color: #76ff33; + text-shadow: 0 0 0.25em rgba(0, 0, 0, 0.5); +} +.xp-text span { color: rgba(255, 255, 255, 0.35); margin-left: 0.2em; } +.xp-icon { + width: 0.85em; height: 0.85em; margin-right: 0.25em; + background: url("/play/static/images/experience.402435da.svg") center / contain no-repeat; +} +.ms-top-logo { + position: absolute; left: 0; right: 0; margin: auto; + display: flex; align-items: center; justify-content: center; + height: 3.75em; pointer-events: none; opacity: 0.95; +} +.ms-top-logo img { height: 3.75em; } +.ms-top-right { display: flex; align-items: center; justify-content: flex-end; height: 6em; z-index: 5; } +.ms-currencies { display: flex; flex-direction: column; justify-content: center; height: 6em; } +.ms-currency { + position: relative; display: flex; align-items: center; gap: 0.4em; + width: 9.4em; height: 3em; padding: 0 0.75em 0 0.6em; cursor: pointer; +} +.ms-currency .coin { width: 1em; height: 1em; } +.ms-currency span { font: 500 1.05em/1 BaseFontMedium, sans-serif; } +.ms-currency .plus { width: 1em; height: 1em; margin-left: auto; opacity: 0.85; } +.ms-currency:hover { background: rgba(255, 255, 255, 0.06); } +.ms-fullscreen { + display: flex; align-items: center; justify-content: center; gap: 0.3em; + width: 6em; height: 6em; cursor: pointer; +} +.ms-fullscreen i { + width: 1.5em; height: 1.5em; background: #bfd5ff; + mask: url("/play/static/images/fullscreen.0b4547f5.svg") center / contain no-repeat; + -webkit-mask: url("/play/static/images/fullscreen.0b4547f5.svg") center / contain no-repeat; +} +.ms-fullscreen b { font: 600 0.75em/1 BaseFontMedium, sans-serif; color: rgba(255, 255, 255, 0.65); } +.ms-fullscreen:hover i { background: #fff; } +.ms-logout { + display: flex; align-items: center; justify-content: center; + width: 6em; height: 6em; border: 0; background: transparent; cursor: pointer; +} +.ms-logout i { + width: 1.5em; height: 1.5em; background: #bfd5ff; + mask: url("/play/static/images/iconBack.62869951.svg") center / contain no-repeat; + -webkit-mask: url("/play/static/images/iconBack.62869951.svg") center / contain no-repeat; +} +.ms-logout:hover { background: rgba(255, 255, 255, 0.05); } +.ms-logout:hover i { background: #fff; } + +/* ---------------- 左侧 50% ---------------- */ +.ms-home { position: absolute; inset: 0; z-index: 1; } +.ms-left-gradient { + position: absolute; left: 0; top: 0; bottom: 0; + display: flex; flex-direction: column; align-items: flex-start; + width: 50%; padding-top: 10em; + background: linear-gradient(90deg, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0) 100%); +} +.ms-play { + display: flex; align-items: center; justify-content: center; + width: 21.875em; height: 10.75em; margin-left: 4.375em; + border: 0; border-radius: 0.5rem; cursor: pointer; + background: url("/play/static/images/videoplay.79570900.gif") center / cover no-repeat; + box-shadow: 0 0 0 0.063em rgba(118, 255, 51, 0.25); +} +.ms-play span { + font: 500 2.75em/1 BaseFontMedium, sans-serif; text-transform: uppercase; + color: #76ff33; text-shadow: 0 0.15em 0.3em rgba(0, 0, 0, 0.5); +} +.ms-play:hover { box-shadow: 0 0 0 2px #76ff33; } + +.ms-menu { + list-style: none; margin: 1.1em 0 0 4.375em; padding: 0; + display: flex; flex-direction: column; width: 27em; +} +.ms-menu-item { + display: flex; align-items: center; height: 4.5em; cursor: pointer; +} +.ms-menu-item:hover .ms-menu-icon { background-color: #fff; } +.ms-menu-item:hover span { color: #ffbc09; } +.ms-menu-icon { + width: 3.5em; height: 3.5em; margin-left: 0.625em; background-color: #bfd5ff; + mask-position: center; mask-repeat: no-repeat; mask-size: contain; + -webkit-mask-position: center; -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain; + transition: background-color 0.15s; +} +.ms-menu-icon.icon-containers { mask-image: url("/play/static/images/iconLobbyMenuContainers.f960290f.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuContainers.f960290f.svg"); } +.ms-menu-icon.icon-quests { mask-image: url("/play/static/images/iconLobbyMenuQuests.54801b8b.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuQuests.54801b8b.svg"); } +.ms-menu-icon.icon-garage { mask-image: url("/play/static/images/iconLobbyMenuGarage.0b623165.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuGarage.0b623165.svg"); } +.ms-menu-icon.icon-shop { mask-image: url("/play/static/images/iconLobbyMenuStore.e755fe14.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuStore.e755fe14.svg"); } +.ms-menu-icon.icon-settings { mask-image: url("/play/static/images/iconLobbyMenuSettings.cc00d08a.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuSettings.cc00d08a.svg"); } +.ms-menu-item span { + margin-left: 0.5625em; font: 500 2em/1 BaseFontMedium, sans-serif; + text-transform: uppercase; text-shadow: 0 0.25em 0.25em rgba(0, 0, 0, 0.25); + transition: color 0.15s; +} + +.ms-footer { flex: 1; display: flex; align-items: flex-end; width: 100%; } +.ms-footer ul { + display: flex; align-items: center; list-style: none; + margin: 0 0 0.8em 5em; padding: 0; height: 5.0625em; +} +.ms-footer-item { + display: flex; align-items: center; justify-content: center; + width: 5em; height: 5em; cursor: pointer; +} +.ms-footer-item:hover { border-radius: 6.25em; box-shadow: 0 0 0 0.125em #fff; } +.ms-footer-icon { + width: 2em; height: 2em; background: #bfd5ff; + mask-position: center; mask-repeat: no-repeat; mask-size: contain; + -webkit-mask-position: center; -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain; +} +.ms-footer-item:hover .ms-footer-icon { background: #fff; } +.ms-footer-icon.icon-news { mask-image: url("/play/static/images/iconLobbyMenuNews.6efe9ae3.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuNews.6efe9ae3.svg"); } +.ms-footer-icon.icon-chat { mask-image: url("/play/static/images/iconLobbyMenuChat.e48a39a4.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuChat.e48a39a4.svg"); } +.ms-footer-icon.icon-friends { mask-image: url("/play/static/images/iconLobbyMenuFriends.28305317.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuFriends.28305317.svg"); } +.ms-footer-icon.icon-clans { mask-image: url("/play/static/images/iconLobbyMenuClan.f01ae646.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuClan.f01ae646.svg"); } + +/* ---------------- 右侧 ---------------- */ +.ms-right-gradient { + position: absolute; right: 0; top: 0; bottom: 0; width: 27.75em; + background: linear-gradient(90deg, #000 0%, rgba(0, 0, 0, 0) 100%); + opacity: 0.75; transform: rotate(180deg); pointer-events: none; +} +.ms-battle-pass { + position: absolute; right: 2.5em; top: 8.5em; + width: 22.5em; height: 10em; padding: 0.5em; box-sizing: border-box; + background: linear-gradient(0deg, rgba(0, 25, 38, 0.25) 0%, #001926 100%); + border-radius: 0.5rem; cursor: pointer; z-index: 1; + filter: drop-shadow(0 0.25rem 0.25rem rgba(0, 0, 0, 0.25)); +} +.ms-battle-pass:hover { box-shadow: 0 0 0 0.125em #bfd5ff; } +.ms-bp-head { + display: flex; align-items: center; justify-content: space-between; + height: 2.75em; font-size: 0.9375em; +} +.ms-bp-head span { display: flex; align-items: center; gap: 0.3em; } +.ms-bp-head img { width: 1em; height: 1em; } +.ms-bp-row { + display: flex; align-items: center; height: 3.125em; margin: 0 0.3em 0.3em; + border: 0.063em solid rgba(255, 255, 255, 0.1); border-radius: 0.3em; + background: rgba(0, 0, 0, 0.1); +} +.ms-bp-row i { + width: 4em; height: 2.4em; margin: 0 0.5em; + background: url("/play/static/images/StarMission.d11624d7.svg") center / contain no-repeat; + opacity: 0.9; +} +.ms-bp-row div { display: flex; flex-direction: column; font-size: 0.9375em; } +.ms-bp-row b { font: 500 1em/1.2 BaseFontBold, sans-serif; } +.ms-bp-row span { font-size: 0.8em; } +.ms-bp-row.locked { opacity: 0.55; } +.ms-bp-row.locked i { filter: grayscale(1); } +.ms-announce { + position: absolute; right: 2.5em; bottom: 2.5em; + display: flex; flex-direction: column; width: 22.5em; min-height: 12.5em; + background: rgba(0, 25, 38, 0.35); border: 0.063em solid rgba(255, 255, 255, 0.25); + border-radius: 0.5rem; overflow: hidden; backdrop-filter: blur(0.78em); +} +.ms-announce:hover { border-color: #bfd5ff; } +.ms-announce-art { + height: 9.5em; + background: + radial-gradient(60% 70% at 50% 40%, rgba(255, 179, 0, 0.18), transparent 70%), + url("/play/static/images/background.fc2779c1.webp") center / cover no-repeat; +} +.ms-announce-title { + padding: 0.7em 0.9em; font: 500 1.125em/1.4 BaseFontMedium, sans-serif; + background: linear-gradient(rgba(0, 25, 38, 0), #001926 60%); +} + +/* ---------------- 屏幕 / 返回 / 底部信息 ---------------- */ +.ms-content { + position: absolute; inset: 0; z-index: 6; + padding: 7em 3em 2.5em; box-sizing: border-box; overflow: auto; + background: linear-gradient(180deg, rgba(0, 12, 20, 0.86), rgba(0, 8, 14, 0.94)); +} +.hidden { display: none !important; } +.ms-back { + position: absolute; top: 6.8em; left: 1.6em; z-index: 8; + padding: 0.55em 1.1em; border-radius: 0.4em; cursor: pointer; + border: 1px solid rgba(255, 255, 255, 0.25); background: rgba(4, 12, 20, 0.7); + color: #cfe0f0; font: 500 0.95em/1 BaseFontMedium, sans-serif; +} +.ms-back:hover { border-color: #fff; color: #fff; } +.ms-screen { max-width: 1120px; margin: 0 auto; } +.ms-screen-title { margin: 0 0 1em; font: 600 2em/1.2 BaseFontMedium, sans-serif; } +.ms-screen-title small { margin-left: 0.6em; color: rgba(255, 255, 255, 0.5); font-size: 0.55em; font-weight: 400; } +.ms-empty { + padding: 3em 1.5em; text-align: center; color: rgba(255, 255, 255, 0.55); + border: 1px dashed rgba(255, 255, 255, 0.18); border-radius: 0.5em; +} +.ms-chat-box { + height: 24em; overflow: auto; padding: 0.7em; border-radius: 0.5em; + background: rgba(0, 8, 14, 0.7); font: 12px/1.55 Consolas, monospace; +} +.ms-log-line { padding: 0.1em 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); color: #b8c9da; } +.ms-news-list { display: flex; flex-direction: column; gap: 0.8em; } +.ms-news { + padding: 1em; border-radius: 0.5em; border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(4, 14, 22, 0.6); +} +.ms-news b { display: block; margin-bottom: 0.4em; } +.ms-news p { margin: 0; color: rgba(255, 255, 255, 0.6); font-size: 0.85em; white-space: pre-wrap; } +.ms-client-info { + position: absolute; right: 0; bottom: 0; height: 1.3rem; z-index: 101; + display: flex; align-items: center; justify-content: flex-end; + padding-right: 1.8rem; pointer-events: none; + font-size: 0.9375em; letter-spacing: 0.0375em; color: rgba(255, 255, 255, 0.7); +} +.ms-client-info i { margin: 0 0.4rem; font-style: normal; color: rgba(255, 255, 255, 0.6); } + +/* ---------------- React 车库(尺寸随根字号) ---------------- */ +.ms-garage-screen { padding-top: 0; } +#garage-root { height: calc(100vh - 9.5em); min-height: 28em; } +.garage { + display: flex; flex-direction: column; gap: 0.9em; height: 100%; min-height: 0; +} +.garage-head { display: flex; align-items: center; justify-content: space-between; gap: 0.9em; } +.garage-head h2 { margin: 0; font: 600 1.7em/1.2 BaseFontMedium, sans-serif; color: #fff; } +.garage-head h2 small { margin-left: 0.5em; color: rgba(255, 255, 255, 0.45); font-size: 0.5em; font-weight: 400; } +.garage-search { + width: 18em; padding: 0.55em 0.9em; border-radius: 0.4em; + border: 1px solid rgba(255, 255, 255, 0.16); background: rgba(3, 10, 16, 0.72); + color: #fff; outline: none; font-size: 0.9em; +} +.garage-search:focus { border-color: rgba(255, 179, 0, 0.6); } +.garage-tabs { display: flex; flex-wrap: wrap; gap: 0.45em; } +.garage-tab { + display: inline-flex; align-items: center; gap: 0.5em; + padding: 0.45em 0.9em; border-radius: 0.35em; font-size: 0.85em; + border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(4, 12, 20, 0.62); color: #c7d7e6; +} +.garage-tab span { color: rgba(255, 255, 255, 0.45); font-size: 0.8em; } +.garage-tab:hover { border-color: rgba(255, 255, 255, 0.3); } +.garage-tab.active { + border-color: #8a5d00; color: #241800; font-weight: 700; + background: linear-gradient(180deg, #ffc93d, #f29500); +} +.garage-tab.active span { color: #4a3200; } +.garage-main { flex: 1; min-height: 0; display: flex; gap: 1em; } +.garage-grid { + flex: 1; min-width: 0; overflow: auto; align-content: start; + display: grid; grid-template-columns: repeat(auto-fill, minmax(9.5em, 1fr)); gap: 0.55em; padding-right: 0.3em; +} +.garage-card { + position: relative; display: flex; flex-direction: column; align-items: stretch; + padding: 0; border-radius: 0.4em; overflow: hidden; text-align: left; + border: 1px solid rgba(255, 255, 255, 0.1); + border-top: 2px solid var(--rarity, #9aa3b2); + background: linear-gradient(180deg, rgba(20, 34, 48, 0.92), rgba(6, 12, 18, 0.94)); + color: #fff; cursor: pointer; +} +.garage-card:hover { border-color: var(--rarity, #9aa3b2); transform: translateY(-1px); } +.garage-card.selected { box-shadow: 0 0 0 1px var(--rarity, #ffb300), 0 0 1.2em rgba(255, 179, 0, 0.18); } +.garage-card-art { + height: 4.6em; display: flex; align-items: center; justify-content: center; + font: 800 1.9em/1 BaseFontBold, sans-serif; color: var(--rarity, #9aa3b2); + background: radial-gradient(closest-side, color-mix(in srgb, var(--rarity, #9aa3b2) 38%, transparent), transparent 72%); +} +.garage-card-name { padding: 0 0.6em; font-size: 0.85em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.garage-card-meta { + padding: 0.15em 0.6em 0.55em; display: flex; align-items: center; justify-content: space-between; gap: 0.4em; + color: rgba(255, 255, 255, 0.5); font-size: 0.72em; +} +.garage-price { color: #ffd76a; } +.garage-premium { + position: absolute; top: 0.35em; right: 0.35em; padding: 0 0.35em; border-radius: 0.2em; + background: #ffb300; color: #241800; font: 800 0.65em/1.1em BaseFontBold, sans-serif; +} +.garage-empty { grid-column: 1 / -1; padding: 3em; text-align: center; color: rgba(255, 255, 255, 0.55); } +.garage-detail { + width: 22em; flex: 0 0 22em; display: flex; flex-direction: column; gap: 0.55em; + padding: 0.8em; border-radius: 0.5em; border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(6, 14, 22, 0.78); +} +.garage-detail h3 { margin: 0 0 0.2em; font: 600 1.25em/1.3 BaseFontMedium, sans-serif; } +.garage-preview { + height: 14em; border-radius: 0.4em; border: 1px solid rgba(255, 255, 255, 0.08); + background: + radial-gradient(60% 60% at 50% 45%, rgba(63, 159, 224, 0.16), transparent 70%), + linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent); +} +.garage-preview-label { color: rgba(255, 255, 255, 0.45); font-size: 0.72em; text-align: center; margin-top: -0.2em; } +.garage-props { display: flex; flex-direction: column; gap: 0.3em; margin-top: 0.5em; } +.garage-prop { + display: flex; align-items: center; justify-content: space-between; + padding: 0.35em 0.55em; border-radius: 0.25em; background: rgba(255, 255, 255, 0.03); font-size: 0.82em; +} +.garage-prop span { color: rgba(255, 255, 255, 0.5); } +.garage-prop b { font-weight: 600; } + +.rarity-0 { --rarity: #9aa3b2; } +.rarity-1 { --rarity: #5fbf6a; } +.rarity-2 { --rarity: #3f8fe0; } +.rarity-3 { --rarity: #a05fe0; } +.rarity-4 { --rarity: #e0a13f; } +.rarity-5 { --rarity: #e05f5f; } + +@media (max-width: 900px) { + .ms-top-logo { display: none; } + .ms-battle-pass, .ms-announce { display: none; } + .garage-detail { display: none; } + .ms-content { padding: 7em 1em 2em; } +} + +/* ---------------- 车库(原版布局) ---------------- */ +.ms-content { padding: 0; } +.ms-screen { padding: 7em 3em 2.5em; box-sizing: border-box; } +.ms-garage-screen { padding: 0; height: 100%; } +#garage-root { height: 100%; min-height: 0; } +.g2 { + display: flex; flex-direction: column; height: 100%; min-height: 0; + background: linear-gradient(180deg, rgba(0, 12, 20, 0.35), rgba(0, 8, 14, 0.75)); +} +.g2-head { + display: flex; align-items: center; height: 6em; flex: 0 0 6em; + border-bottom: 1px solid rgba(255, 255, 255, 0.25); + background: linear-gradient(180deg, rgba(0, 12, 20, 0.6), rgba(0, 12, 20, 0.12)); +} +.g2-back { + width: 6em; height: 6em; display: flex; align-items: center; justify-content: center; + border: 0; background: transparent; cursor: pointer; +} +.g2-back:hover { background: rgba(255, 255, 255, 0.05); } +.g2-back-icon { + width: 1.5em; height: 1.2em; background: #bfd5ff; + mask: url("/play/static/images/iconBackArrow.9f950ca9.svg") center / contain no-repeat; + -webkit-mask: url("/play/static/images/iconBackArrow.9f950ca9.svg") center / contain no-repeat; +} +.g2-title { + position: absolute; left: 0; right: 0; margin: auto; width: 12em; text-align: center; + font: 500 1.4em/1 BaseFontMedium, sans-serif; text-transform: uppercase; + pointer-events: none; +} +.g2-currencies { margin-left: auto; display: flex; align-items: center; gap: 1.2em; padding-right: 1.5em; font: 500 1em/1 BaseFontMedium, sans-serif; } +.g2-currencies span { display: inline-flex; align-items: center; gap: 0.35em; } +.g2-currencies .coin { width: 1.1em; height: 1.1em; } + +.g2-body { flex: 1; min-height: 0; display: flex; gap: 1em; padding: 4em 6em; box-sizing: border-box; } +.g2-left { width: 22em; flex: 0 0 22em; display: flex; flex-direction: column; gap: 0.35em; } +.g2-gs { + display: flex; align-items: center; gap: 0.5em; margin-bottom: 0.5em; + padding: 0.6em 0.8em; border-radius: 0.4em; background: rgba(0, 20, 32, 0.55); + border: 1px solid rgba(255, 255, 255, 0.12); +} +.g2-gs img { width: 2.2em; height: 1.1em; } +.g2-gs b { font: 700 1.8em/1 BaseFontBold, sans-serif; color: #fff; } +.g2-param { + display: flex; align-items: center; justify-content: space-between; + padding: 0.45em 0.7em; border-radius: 0.3em; background: rgba(3, 12, 20, 0.5); + border: 1px solid rgba(255, 255, 255, 0.06); font-size: 0.85em; +} +.g2-param span { color: rgba(255, 255, 255, 0.55); } +.g2-resists { display: flex; gap: 0.5em; margin-top: auto; } +.g2-resist { + width: 4em; height: 5em; display: flex; align-items: center; justify-content: center; + border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 0.4em; + background: rgba(0, 18, 30, 0.5); color: rgba(255, 255, 255, 0.35); +} + +.g2-scene { + flex: 1; min-width: 0; position: relative; display: flex; flex-direction: column; + align-items: center; justify-content: center; border-radius: 0.5em; + border: 1px solid rgba(255, 255, 255, 0.06); + background: + radial-gradient(60% 55% at 50% 45%, rgba(63, 159, 224, 0.14), transparent 70%), + linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent); +} +.g2-scene-art { + font: 800 16em/1 BaseFontBold, sans-serif; color: rgba(63, 159, 224, 0.18); + text-shadow: 0 0 0.2em rgba(0, 0, 0, 0.4); user-select: none; +} +.g2-scene-caption { + position: absolute; bottom: 1.2em; left: 0; right: 0; text-align: center; + font: 500 1em/1.4 BaseFontMedium, sans-serif; color: rgba(255, 255, 255, 0.75); +} +.g2-scene-caption span { display: block; font-size: 0.65em; color: rgba(255, 255, 255, 0.35); } +.g2-drawer-toggle { + position: absolute; left: 1em; bottom: 1em; padding: 0.6em 1.4em; + border-radius: 0.4em; border: 1px solid #8a5d00; cursor: pointer; + background: linear-gradient(180deg, #ffc93d, #f29500); color: #241800; + font: 700 0.9em/1 BaseFontBold, sans-serif; +} + +.g2-right { width: 23em; flex: 0 0 23em; display: grid; grid-template-columns: 1fr 1fr; gap: 0.6em; align-content: start; } +.g2-mounted { + display: flex; flex-direction: column; padding: 0.5em; border-radius: 0.4em; + border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(3, 12, 20, 0.55); +} +.g2-mounted-title { font-size: 0.72em; color: rgba(255, 255, 255, 0.5); text-transform: uppercase; } +.g2-mounted-art { + height: 4.6em; display: flex; align-items: center; justify-content: center; + font: 800 1.8em/1 BaseFontBold, sans-serif; color: var(--rarity, #9aa3b2); + background: radial-gradient(closest-side, color-mix(in srgb, var(--rarity, #9aa3b2) 30%, transparent), transparent 72%); +} +.g2-mounted-name { font-size: 0.8em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.g2-supplies { + flex: 0 0 9.6em; display: flex; align-items: center; justify-content: center; gap: 1.2em; + border-top: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(0, 10, 18, 0.55); +} +.g2-supply { + width: 7.5em; height: 4em; display: flex; align-items: center; gap: 0.5em; + justify-content: center; border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 0.35em; background: rgba(3, 12, 20, 0.6); font-size: 0.72em; +} +.g2-supply img { width: 1.8em; height: 1.8em; object-fit: contain; } +.g2-supply span { color: rgba(255, 255, 255, 0.6); } +.g2-supply b { color: #fff; } + +.g2-drawer { + position: absolute; inset: 6em 0 0 0; z-index: 12; display: flex; flex-direction: column; + padding: 1em 2em 1.5em; background: rgba(2, 8, 14, 0.94); backdrop-filter: blur(6px); +} +.g2-drawer-head { display: flex; align-items: center; gap: 1em; margin-bottom: 0.8em; } +.g2-drawer-head b { font: 600 1.3em/1 BaseFontMedium, sans-serif; } +.g2-drawer-head .garage-search { margin-left: auto; } +.g2-drawer-close { + padding: 0.5em 1.2em; border-radius: 0.35em; border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(255, 255, 255, 0.05); color: #fff; cursor: pointer; +} +.g2-drawer-grid { flex: 1; min-height: 0; overflow: auto; margin-top: 0.8em; } + +@media (max-width: 900px) { + .g2-left, .g2-right { display: none; } + .g2-supplies { gap: 0.4em; } + .g2-supply { width: auto; padding: 0 0.5em; } + .g2-supply span { display: none; } +} + +/* 顶栏:VIP / 组队图标(对照线上 .ksc-38 / .ksc-40) */ +.premium-block { + position: relative; width: 6em; height: 6em; + display: flex; align-items: center; justify-content: center; + background: radial-gradient(100% 100% at 0% 0%, rgba(255, 204, 0, 0.75) 0%, rgba(255, 38, 74, 0.5) 50%, rgba(255, 38, 74, 0) 100%); +} +.premium-block.off { opacity: 0.35; filter: grayscale(0.6); } +.premium-icon { + width: 2em; height: 2em; + background: url("/play/static/images/iconButtonPremium.bc17a592.svg") center / contain no-repeat; +} +.group-block { + position: relative; width: 6em; height: 6em; + display: flex; align-items: center; justify-content: center; +} +.group-icon { + position: absolute; width: 1.5em; height: 1.5em; + background: rgba(191, 212, 255, 0.5); + mask: url("/play/static/images/groupUser.67c9ec61.svg") center / contain no-repeat; + -webkit-mask: url("/play/static/images/groupUser.67c9ec61.svg") center / contain no-repeat; +} +.group-icon.g1 { transform: translateX(-0.5em); } +.group-icon.g2 { transform: translateX(0.5em); } +.premium-block + .group-block + .rank-icon { border-left: 1px solid rgba(255, 255, 255, 0.25); } + +/* 车库全屏:3D 覆盖整页,左右参数/配件与底部道具作为浮层,header 保留 */ +.g2 { position: relative; } +.g2-body { + position: absolute; inset: 6em 0 0 0; display: block; padding: 0; gap: 0; +} +.g2-scene { + position: absolute; inset: 0; border: 0; border-radius: 0; background: #080f16; +} +.g2-left { + position: absolute; left: 2em; top: 2em; width: 22em; max-height: calc(100% - 4em); + overflow: auto; padding: 1em; box-sizing: border-box; + background: rgba(4, 14, 22, 0.82); border: 1px solid rgba(255, 255, 255, 0.10); + border-radius: 0.5em; backdrop-filter: blur(6px); +} +.g2-right { + position: absolute; right: 2em; top: 2em; width: 23em; padding: 1em; + box-sizing: border-box; background: rgba(4, 14, 22, 0.82); + border: 1px solid rgba(255, 255, 255, 0.10); border-radius: 0.5em; + backdrop-filter: blur(6px); +} +.g2-supplies { + position: absolute; left: 0; right: 0; bottom: 0; height: 9.6em; + background: rgba(3, 10, 16, 0.82); backdrop-filter: blur(6px); +} +.g2-scene-caption { bottom: 10.5em; pointer-events: none; } +.g2-resists { margin-top: 1em; } + +.g2-head { position: relative; z-index: 5; } +.g2-scene { z-index: 0; } +.g2-left, .g2-right { z-index: 3; } +.g2-supplies { z-index: 3; } +.g2-drawer-toggle { z-index: 4; } + +.g2-exit { width: 6em; height: 6em; display: flex; align-items: center; justify-content: center; border: 0; background: transparent; cursor: pointer; } +.g2-exit:hover { background: rgba(255,255,255,.05); } +.g2-exit-icon { width: 1.5em; height: 1.5em; background: #bfd5ff; mask: url("/play/static/images/iconBack.62869951.svg") center / contain no-repeat; -webkit-mask: url("/play/static/images/iconBack.62869951.svg") center / contain no-repeat; } +.g2-exit:hover .g2-exit-icon { background: #fff; } diff --git a/TankiOffline/tools/res_proxy.js b/TankiOffline/tools/res_proxy.js new file mode 100644 index 0000000..d220f61 --- /dev/null +++ b/TankiOffline/tools/res_proxy.js @@ -0,0 +1,145 @@ +// res_proxy.js —— 本地资源代理(res.3dtank.com → 本地缓存) +// +// 为什么需要:官方 3DS 贴图(BG1 1024.PNG / SKY1.PNG 等)没有 CORS 头, +// 浏览器从 localhost 直接请求会被拦截;同时每次开发都走公网很慢。 +// +// 用法:node tools/res_proxy.js [--port 8082] [--cache build/res-cache] +// GET /res/ → https://res.3dtank.com/,落盘缓存后返回 +// GET /play/ → 3DTank/web/play/(本地镜像,可选) +// GET /health → ok +// +// 前端通过 webpack devServer 把 /res 代理到本服务(webpack.config.d/res-proxy.js)。 +'use strict'; + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const { URL } = require('url'); + +const Argv = process.argv.slice(2); +const Arg = (Name, Def) => { + const I = Argv.indexOf('--' + Name); + return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def; +}; + +const Root = path.resolve(__dirname, '..'); +const Port = Number(Arg('port', '8082')); +const CacheDir = path.resolve(Root, Arg('cache', 'build/res-cache')); +const MirrorPlay = path.resolve(Root, '..', '3DTank', 'web', 'play'); +const Upstream = 'https://res.3dtank.com'; + +const Mime = { + '.3ds': 'application/octet-stream', + '.a3d': 'application/octet-stream', + '.webp': 'image/webp', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.svg': 'image/svg+xml', + '.json': 'application/json', + '.info': 'application/octet-stream', + '.mp3': 'audio/mpeg', + '.ktx': 'application/octet-stream', +}; + +fs.mkdirSync(CacheDir, { recursive: true }); + +function CachePath(Key) { + const Clean = Key.replace(/^\/+/, '').replace(/[^a-zA-Z0-9._/-]/g, '_').replace(/\.\./g, '_'); + return path.join(CacheDir, Clean); +} + +function Send(Res, Status, Type, Body) { + Res.writeHead(Status, { + 'Content-Type': Type || 'application/octet-stream', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': '*', + 'Cache-Control': 'public, max-age=31536000, immutable', + }); + Res.end(Body); +} + +async function FetchUpstream(PathAndQuery) { + const Decoded = decodeURIComponent(PathAndQuery); + const Ext = path.extname(Decoded); + const NoExt = Decoded.slice(0, Decoded.length - Ext.length); + const Candidates = [ + PathAndQuery, + Decoded.toLowerCase(), + (NoExt + '.webp').toLowerCase(), + (NoExt + '.png').toLowerCase(), + (NoExt + '.webp'), + (NoExt + '.png'), + ]; + for (const PathTry of [...new Set(Candidates)]) { + const R = await fetch(Upstream + (PathTry.startsWith('/') ? PathTry : '/' + PathTry), { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) TankiOffline/0.1', + Accept: '*/*', + }, + }); + if (R.ok) { + const Body = Buffer.from(await R.arrayBuffer()); + return { + Status: 200, + Type: R.headers.get('content-type') || 'application/octet-stream', + Body, + Resolved: PathTry, + }; + } + } + return { Status: 404, Type: 'text/plain', Body: Buffer.from('upstream 404') }; +} + +const Server = http.createServer(async (Req, Res) => { + if (Req.method === 'OPTIONS') { + Send(Res, 204, 'text/plain', Buffer.alloc(0)); + return; + } + const U = new URL(Req.url, 'http://localhost'); + if (U.pathname === '/health') { + Send(Res, 200, 'text/plain', Buffer.from('ok')); + return; + } + + // 本地镜像:/play/... → 3DTank/web/play/... + if (U.pathname.startsWith('/play/')) { + const File = path.join(MirrorPlay, U.pathname.slice('/play/'.length)); + if (File.startsWith(MirrorPlay) && fs.existsSync(File) && fs.statSync(File).isFile()) { + Send(Res, 200, Mime[path.extname(File).toLowerCase()], fs.readFileSync(File)); + return; + } + Send(Res, 404, 'text/plain', Buffer.from('mirror miss')); + return; + } + + // 线上资源:/res/ → res.3dtank.com/ + if (!U.pathname.startsWith('/res/')) { + Send(Res, 404, 'text/plain', Buffer.from('not found')); + return; + } + const Key = U.pathname.slice('/res'.length); + const Cache = CachePath(Key); + if (fs.existsSync(Cache) && fs.statSync(Cache).isFile()) { + Send(Res, 200, Mime[path.extname(Cache).toLowerCase()] || 'application/octet-stream', fs.readFileSync(Cache)); + return; + } + try { + const Result = await FetchUpstream(Key); + if (Result.Status === 200) { + fs.mkdirSync(path.dirname(Cache), { recursive: true }); + fs.writeFileSync(Cache, Result.Body); + } + Send(Res, Result.Status, Result.Type, Result.Body); + console.log(`${Result.Status === 200 ? 'cache' : 'fail '} ${Key} (${Result.Body.length}B)`); + } catch (E) { + Send(Res, 502, 'text/plain', Buffer.from('proxy error: ' + E.message)); + console.log('error', Key, E.message); + } +}); + +Server.listen(Port, '127.0.0.1', () => { + console.log(`res proxy: http://127.0.0.1:${Port}/res/... (cache: ${CacheDir})`); + console.log(`play mirror: http://127.0.0.1:${Port}/play/... (${MirrorPlay})`); +}); diff --git a/TankiOffline/webpack.config.d/res-proxy.js b/TankiOffline/webpack.config.d/res-proxy.js new file mode 100644 index 0000000..4acfc6a --- /dev/null +++ b/TankiOffline/webpack.config.d/res-proxy.js @@ -0,0 +1,10 @@ +// res-proxy.js —— 开发服务器把 /res/* 转给本地资源代理(tools/res_proxy.js) +config.devServer = config.devServer || {}; +config.devServer.proxy = (config.devServer.proxy || []).concat([ + { + context: ['/res'], + target: 'http://127.0.0.1:8082', + changeOrigin: true, + pathRewrite: { '^/res': '' }, + }, +]);