feat(ui): 本地资源代理 + 车库全屏布局与退出入口
This commit is contained in:
1 parent
87961f6209
commit
df28be81f6
8 files changed
+944
-15
No files matched your search
@@ -78,6 +78,7 @@ private val GarageScreen = FC<Props> {
|
||||
|
||||
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<Props> {
|
||||
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<Props> {
|
||||
|
||||
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<Props> {
|
||||
|
||||
aside {
|
||||
Css("g2-right")
|
||||
style = js("({ zIndex: 3 })")
|
||||
MountedBlock("无人机", Drone)
|
||||
MountedBlock("手榴弹", Grenade)
|
||||
MountedBlock("炮塔", Turret)
|
||||
@@ -140,6 +148,7 @@ private val GarageScreen = FC<Props> {
|
||||
|
||||
div {
|
||||
Css("g2-supplies")
|
||||
style = js("({ zIndex: 3 })")
|
||||
Supply("Repair.13e5e240.svg", "维修")
|
||||
Supply("armor.b0b6d887.svg", "护甲")
|
||||
Supply("damage.af4f0289.svg", "伤害")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// Garage3D.js —— 车库 3D(Three.js 加载官方 3DS 场景 + 占位坦克)
|
||||
// 由 index.html 以 <script type="module"> 加载;import map 把 'three' 指到本地镜像。
|
||||
import * as THREE from 'three';
|
||||
import { TDSLoader } from '/vendor/three/TDSLoader.js';
|
||||
import { OrbitControls } from '/vendor/three/OrbitControls.js';
|
||||
|
||||
const MapUrl = 'http://127.0.0.1:8082/res/613/2501/252/46/32047724143075/object.3ds';
|
||||
let Current = null;
|
||||
|
||||
function PlaceholderTank() {
|
||||
const G = new THREE.Group();
|
||||
const HullMat = new THREE.MeshStandardMaterial({ color: 0x4c5a3a, roughness: 0.65, metalness: 0.35 });
|
||||
const DarkMat = new THREE.MeshStandardMaterial({ color: 0x1a1d20, roughness: 0.9 });
|
||||
const Hull = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.55, 1.25), HullMat);
|
||||
Hull.position.y = 0.55;
|
||||
Hull.castShadow = true;
|
||||
G.add(Hull);
|
||||
const Turret = new THREE.Mesh(new THREE.CylinderGeometry(0.48, 0.55, 0.34, 24), HullMat);
|
||||
Turret.position.set(0, 0.98, 0.05);
|
||||
Turret.castShadow = true;
|
||||
G.add(Turret);
|
||||
const Barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.07, 1.55, 12), DarkMat);
|
||||
Barrel.rotation.x = Math.PI / 2;
|
||||
Barrel.position.set(0, 0.98, 0.9);
|
||||
G.add(Barrel);
|
||||
for (const X of [-1.02, 1.02]) {
|
||||
const Track = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.52, 1.55), DarkMat);
|
||||
Track.position.set(X, 0.36, 0);
|
||||
Track.castShadow = true;
|
||||
G.add(Track);
|
||||
}
|
||||
return G;
|
||||
}
|
||||
|
||||
function Mount(CanvasId) {
|
||||
if (Current && Current.scene) return;
|
||||
const canvas = document.getElementById(CanvasId);
|
||||
if (!canvas) {
|
||||
if (Mount.Retry === undefined) Mount.Retry = 0;
|
||||
if (Mount.Retry++ < 100) setTimeout(() => Mount(CanvasId), 100);
|
||||
return;
|
||||
}
|
||||
Mount.Retry = 0;
|
||||
|
||||
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(40, 1, 0.1, 200);
|
||||
camera.position.set(7, 4.5, 9);
|
||||
|
||||
const controls = new OrbitControls(camera, canvas);
|
||||
controls.target.set(0, 2.0, 0);
|
||||
controls.enablePan = false;
|
||||
controls.enableZoom = true;
|
||||
controls.minDistance = 5;
|
||||
controls.maxDistance = 26;
|
||||
controls.autoRotate = true;
|
||||
controls.autoRotateSpeed = 0.7;
|
||||
|
||||
scene.add(new THREE.HemisphereLight(0xbfd5ff, 0x0a141e, 2.2));
|
||||
const Sun = new THREE.DirectionalLight(0xffffff, 2.4);
|
||||
Sun.position.set(4, 7, 5);
|
||||
Sun.castShadow = true;
|
||||
Sun.shadow.mapSize.set(1024, 1024);
|
||||
scene.add(Sun);
|
||||
const Rim = new THREE.DirectionalLight(0x3f9fe0, 1.0);
|
||||
Rim.position.set(-5, 3, -4);
|
||||
scene.add(Rim);
|
||||
|
||||
const Floor = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(4.2, 64),
|
||||
new THREE.MeshStandardMaterial({ color: 0x0d1a24, roughness: 0.9, metalness: 0.1 }),
|
||||
);
|
||||
Floor.rotation.x = -Math.PI / 2;
|
||||
Floor.receiveShadow = true;
|
||||
scene.add(Floor);
|
||||
|
||||
const Platform = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(2.1, 2.25, 0.18, 64),
|
||||
new THREE.MeshStandardMaterial({ color: 0x1b2c3a, roughness: 0.6, metalness: 0.5 }),
|
||||
);
|
||||
Platform.position.y = 0.09;
|
||||
Platform.receiveShadow = true;
|
||||
Platform.castShadow = true;
|
||||
scene.add(Platform);
|
||||
|
||||
// 真实坦克模型待 Item3DCC.Resource 索引接入;先不放几何占位,避免误导。
|
||||
|
||||
const ShowMap = false;
|
||||
if (ShowMap) new TDSLoader().load(
|
||||
MapUrl,
|
||||
(Obj) => {
|
||||
Obj.traverse((N) => {
|
||||
if (N.isMesh) {
|
||||
N.castShadow = true;
|
||||
N.receiveShadow = true;
|
||||
if (N.material) {
|
||||
N.material = new THREE.MeshStandardMaterial({
|
||||
color: N.material.color ? N.material.color.getHex() : 0x6f7b86,
|
||||
roughness: 0.8,
|
||||
metalness: 0.15,
|
||||
opacity: N.material.opacity === undefined ? 1 : N.material.opacity,
|
||||
transparent: !!N.material.transparent,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
const Box = new THREE.Box3().setFromObject(Obj);
|
||||
const Size = Box.getSize(new THREE.Vector3());
|
||||
const Center = Box.getCenter(new THREE.Vector3());
|
||||
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);
|
||||
},
|
||||
undefined,
|
||||
() => {},
|
||||
);
|
||||
|
||||
function Resize() {
|
||||
const Rect = (canvas.parentElement || canvas).getBoundingClientRect();
|
||||
const W = Math.max(1, Math.floor(Rect.width));
|
||||
const H = Math.max(1, Math.floor(Rect.height));
|
||||
if (canvas.width !== W || canvas.height !== H) {
|
||||
renderer.setSize(W, H, false);
|
||||
camera.aspect = W / H;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
}
|
||||
const Observer = new ResizeObserver(Resize);
|
||||
Observer.observe(canvas.parentElement || canvas);
|
||||
Resize();
|
||||
|
||||
let Alive = true;
|
||||
function Frame() {
|
||||
if (!Alive) return;
|
||||
Resize();
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
requestAnimationFrame(Frame);
|
||||
}
|
||||
Frame();
|
||||
|
||||
Current = {
|
||||
scene,
|
||||
camera,
|
||||
renderer,
|
||||
dispose() {
|
||||
Alive = false;
|
||||
Observer.disconnect();
|
||||
controls.dispose();
|
||||
renderer.dispose();
|
||||
Current = null;
|
||||
window.__garage3d = null;
|
||||
},
|
||||
};
|
||||
window.__garage3d = Current;
|
||||
}
|
||||
|
||||
function Dispose() {
|
||||
if (Current) Current.dispose();
|
||||
}
|
||||
|
||||
window.Garage3D = { mount: Mount, dispose: Dispose };
|
||||
@@ -4,12 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>TankiOffline · 大厅</title>
|
||||
<link rel="stylesheet" href="entrance.css" />
|
||||
<link rel="stylesheet" href="lobby.css" />
|
||||
<link rel="stylesheet" href="entrance.css?v=6" />
|
||||
<link rel="stylesheet" href="lobby2.css" />
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "/vendor/three/three.module.js" } }
|
||||
</script>
|
||||
<script type="module" src="/Garage3D.js"></script>
|
||||
<script type="module" src="/Garage3D2.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -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; }
|
||||
@@ -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; }
|
||||
@@ -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/<path> → https://res.3dtank.com/<path>,落盘缓存后返回
|
||||
// GET /play/<path> → 3DTank/web/play/<path>(本地镜像,可选)
|
||||
// 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/<path> → res.3dtank.com/<path>
|
||||
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})`);
|
||||
});
|
||||
@@ -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': '' },
|
||||
},
|
||||
]);
|
||||
Reference in new issue
Block a user