Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WARZONE X β Vivian Harris</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400;700&family=Rajdhani:wght@400;600;700&family=Share+Tech+Mono&display=swap'); | |
| :root { | |
| --red: #e8001d; | |
| --gold: #f0a500; | |
| --dark: #0a0a0a; | |
| --panel: #0d1117; | |
| --border: #1e2a38; | |
| --text: #c9d1d9; | |
| --glow: rgba(232,0,29,0.4); | |
| --blue: #1e90ff; | |
| } | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| body { | |
| background: var(--dark); | |
| font-family: 'Rajdhani', sans-serif; | |
| color: var(--text); | |
| overflow: hidden; | |
| height: 100vh; | |
| width: 100vw; | |
| } | |
| /* βββββββββββββββ SCREENS βββββββββββββββ */ | |
| .screen { display:none; width:100%; height:100vh; position:absolute; top:0; left:0; } | |
| .screen.active { display:flex; } | |
| /* βββββββββββββββ MAIN MENU βββββββββββββββ */ | |
| #main-menu { | |
| background: linear-gradient(135deg, #0a0a0a 0%, #0d1117 50%, #111827 100%); | |
| flex-direction: row; | |
| align-items: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| #main-menu::before { | |
| content:''; | |
| position:absolute; inset:0; | |
| background: | |
| radial-gradient(ellipse 60% 80% at 30% 50%, rgba(232,0,29,0.08) 0%, transparent 70%), | |
| radial-gradient(ellipse 40% 60% at 80% 30%, rgba(240,165,0,0.05) 0%, transparent 70%); | |
| pointer-events:none; | |
| } | |
| .scanlines { | |
| position:absolute; inset:0; pointer-events:none; z-index:1; | |
| background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.15) 2px, rgba(0,0,0,0.15) 4px); | |
| } | |
| .menu-left { | |
| flex:1; display:flex; flex-direction:column; | |
| align-items:center; justify-content:center; | |
| padding:40px; z-index:2; | |
| } | |
| .game-title { | |
| font-family:'Oswald',sans-serif; | |
| font-size:72px; font-weight:700; | |
| letter-spacing:8px; | |
| color:#fff; | |
| text-shadow: 0 0 30px var(--red), 0 0 60px rgba(232,0,29,0.3); | |
| line-height:1; | |
| text-align:center; | |
| } | |
| .game-subtitle { | |
| font-size:14px; letter-spacing:6px; color:var(--gold); | |
| margin-top:8px; text-align:center; | |
| font-family:'Share Tech Mono', monospace; | |
| } | |
| .divider { | |
| width:200px; height:2px; | |
| background: linear-gradient(90deg, transparent, var(--red), transparent); | |
| margin:30px auto; | |
| } | |
| .menu-buttons { display:flex; flex-direction:column; gap:14px; width:280px; } | |
| .menu-btn { | |
| background: linear-gradient(135deg, rgba(14,21,32,0.9), rgba(10,10,10,0.95)); | |
| border: 1px solid var(--border); | |
| border-left: 3px solid var(--red); | |
| color: #fff; | |
| padding: 16px 24px; | |
| font-family:'Oswald',sans-serif; | |
| font-size:18px; | |
| letter-spacing:3px; | |
| cursor:pointer; | |
| transition: all 0.2s; | |
| text-align:left; | |
| clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%); | |
| position:relative; | |
| overflow:hidden; | |
| } | |
| .menu-btn::after { | |
| content:''; | |
| position:absolute; left:-100%; top:0; width:100%; height:100%; | |
| background: linear-gradient(90deg, transparent, rgba(232,0,29,0.15), transparent); | |
| transition: left 0.4s; | |
| } | |
| .menu-btn:hover { border-left-color:var(--gold); transform:translateX(4px); box-shadow:0 0 20px rgba(232,0,29,0.2); } | |
| .menu-btn:hover::after { left:100%; } | |
| .menu-btn .btn-icon { margin-right:12px; color:var(--red); } | |
| /* βββββββββββββββ CHARACTER CARD βββββββββββββββ */ | |
| .menu-right { | |
| width:380px; display:flex; flex-direction:column; | |
| align-items:center; justify-content:center; | |
| padding:40px; z-index:2; border-left:1px solid var(--border); | |
| background: rgba(255,255,255,0.02); | |
| } | |
| .char-card { | |
| background: linear-gradient(160deg, #0d1117, #111827); | |
| border:1px solid var(--border); | |
| border-top:2px solid var(--red); | |
| padding:24px; width:100%; | |
| clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px)); | |
| position:relative; | |
| } | |
| .char-card::before { | |
| content:'OPERATOR'; | |
| position:absolute; top:10px; right:28px; | |
| font-family:'Share Tech Mono',monospace; | |
| font-size:10px; color:var(--red); letter-spacing:3px; | |
| } | |
| .char-avatar { | |
| width:160px; height:160px; margin:0 auto 16px; | |
| position:relative; | |
| } | |
| /* SVG Character Vivian Harris */ | |
| .char-avatar svg { width:100%; height:100%; } | |
| .char-name { | |
| font-family:'Oswald',sans-serif; | |
| font-size:24px; font-weight:700; | |
| color:#fff; text-align:center; | |
| letter-spacing:3px; | |
| } | |
| .char-rank { | |
| font-family:'Share Tech Mono',monospace; | |
| font-size:11px; color:var(--gold); | |
| text-align:center; letter-spacing:4px; margin-top:4px; | |
| } | |
| .char-stats { | |
| display:grid; grid-template-columns:1fr 1fr; | |
| gap:8px; margin-top:16px; | |
| } | |
| .stat-item { background:rgba(255,255,255,0.03); padding:8px 10px; border:1px solid rgba(255,255,255,0.06); } | |
| .stat-label { font-size:10px; color:#666; letter-spacing:2px; } | |
| .stat-val { font-family:'Share Tech Mono',monospace; font-size:16px; color:var(--gold); } | |
| /* βββββββββββββββ MODE SELECT βββββββββββββββ */ | |
| #mode-select { | |
| background: linear-gradient(135deg, #0a0c10, #0d1117); | |
| flex-direction:column; align-items:center; justify-content:center; | |
| gap:40px; | |
| } | |
| .mode-title { | |
| font-family:'Oswald',sans-serif; | |
| font-size:14px; letter-spacing:6px; color:var(--red); | |
| } | |
| .mode-cards { display:flex; gap:30px; } | |
| .mode-card { | |
| width:280px; background: linear-gradient(160deg, #0d1117, #0a0c10); | |
| border:1px solid var(--border); cursor:pointer; | |
| transition: all 0.3s; position:relative; overflow:hidden; | |
| clip-path: polygon(0 0, calc(100% - 24px) 0, 100% 24px, 100% 100%, 24px 100%, 0 calc(100% - 24px)); | |
| } | |
| .mode-card:hover { border-color:var(--red); transform:translateY(-6px); box-shadow:0 20px 40px rgba(232,0,29,0.2); } | |
| .mode-card-img { | |
| height:160px; display:flex; align-items:center; justify-content:center; | |
| font-size:80px; background:rgba(0,0,0,0.3); position:relative; overflow:hidden; | |
| } | |
| .mode-card-img::after { | |
| content:''; position:absolute; inset:0; | |
| background:linear-gradient(180deg, transparent 50%, #0d1117 100%); | |
| } | |
| .mode-card-body { padding:20px; } | |
| .mode-card-name { font-family:'Oswald',sans-serif; font-size:22px; letter-spacing:3px; color:#fff; } | |
| .mode-card-desc { font-size:13px; color:#666; margin-top:6px; line-height:1.5; } | |
| .mode-badge { | |
| position:absolute; top:12px; right:12px; z-index:1; | |
| background:var(--red); color:#fff; | |
| font-family:'Share Tech Mono',monospace; | |
| font-size:10px; padding:3px 8px; letter-spacing:2px; | |
| } | |
| /* βββββββββββββββ LOADOUT SCREEN βββββββββββββββ */ | |
| #loadout-screen { | |
| background: linear-gradient(135deg, #080b10, #0d1117); | |
| flex-direction:column; align-items:center; | |
| padding:40px; gap:30px; overflow-y:auto; | |
| } | |
| .screen-header { | |
| display:flex; align-items:center; gap:20px; width:100%; max-width:900px; | |
| } | |
| .back-btn { | |
| background: rgba(255,255,255,0.04); border:1px solid var(--border); | |
| color:var(--text); padding:8px 16px; cursor:pointer; | |
| font-family:'Rajdhani',sans-serif; font-size:14px; | |
| letter-spacing:2px; transition:all 0.2s; | |
| } | |
| .back-btn:hover { background:rgba(232,0,29,0.1); border-color:var(--red); color:var(--red); } | |
| .screen-title { font-family:'Oswald',sans-serif; font-size:32px; letter-spacing:6px; color:#fff; } | |
| .loadout-grid { display:flex; gap:30px; width:100%; max-width:900px; } | |
| .gun-card { | |
| flex:1; background: linear-gradient(160deg, #0d1117, #0a0c10); | |
| border:2px solid var(--border); cursor:pointer; transition:all 0.3s; | |
| clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 0 100%); | |
| padding:24px; text-align:center; | |
| } | |
| .gun-card.selected { border-color:var(--red); box-shadow:0 0 30px rgba(232,0,29,0.3); } | |
| .gun-card:hover { border-color:var(--gold); } | |
| .gun-name { font-family:'Oswald',sans-serif; font-size:28px; letter-spacing:4px; color:#fff; } | |
| .gun-type { font-size:12px; color:var(--gold); letter-spacing:3px; margin-bottom:16px; font-family:'Share Tech Mono',monospace; } | |
| .gun-art { font-size:60px; margin:20px 0; } | |
| .gun-stats { text-align:left; } | |
| .gun-stat { display:flex; align-items:center; gap:10px; margin:8px 0; } | |
| .gun-stat-label { font-size:12px; color:#666; width:80px; letter-spacing:1px; } | |
| .gun-stat-bar { flex:1; height:4px; background:rgba(255,255,255,0.1); position:relative; } | |
| .gun-stat-fill { height:100%; background:linear-gradient(90deg, var(--red), var(--gold)); transition:width 0.5s; } | |
| .gun-select-indicator { | |
| margin-top:16px; padding:10px; | |
| background:rgba(232,0,29,0.1); border:1px solid var(--red); | |
| font-family:'Share Tech Mono',monospace; font-size:12px; | |
| color:var(--red); letter-spacing:3px; text-align:center; | |
| display:none; | |
| } | |
| .gun-card.selected .gun-select-indicator { display:block; } | |
| .loadout-confirm { | |
| width:100%; max-width:900px; | |
| } | |
| .confirm-btn { | |
| width:100%; padding:16px; | |
| background:linear-gradient(135deg, var(--red), #a00015); | |
| border:none; color:#fff; | |
| font-family:'Oswald',sans-serif; font-size:20px; letter-spacing:6px; | |
| cursor:pointer; transition:all 0.3s; | |
| clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px)); | |
| } | |
| .confirm-btn:hover { background:linear-gradient(135deg, #ff0020, var(--red)); box-shadow:0 0 30px var(--glow); } | |
| .confirm-btn:disabled { background:#333; cursor:not-allowed; box-shadow:none; } | |
| /* βββββββββββββββ BATTLE ROYALE LOBBY βββββββββββββββ */ | |
| #br-lobby { | |
| background: linear-gradient(135deg, #080b10, #0d1117); | |
| flex-direction:column; align-items:center; | |
| padding:40px; gap:24px; overflow-y:auto; | |
| } | |
| .diff-grid { display:flex; gap:16px; } | |
| .diff-btn { | |
| padding:12px 24px; border:1px solid var(--border); | |
| background:rgba(255,255,255,0.03); color:#888; | |
| font-family:'Oswald',sans-serif; font-size:16px; letter-spacing:3px; | |
| cursor:pointer; transition:all 0.2s; | |
| clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%); | |
| } | |
| .diff-btn:hover, .diff-btn.selected { color:#fff; } | |
| .diff-btn[data-diff="noob"].selected { border-color:#4ade80; color:#4ade80; box-shadow:0 0 15px rgba(74,222,128,0.2); } | |
| .diff-btn[data-diff="easy"].selected { border-color:var(--blue); color:var(--blue); box-shadow:0 0 15px rgba(30,144,255,0.2); } | |
| .diff-btn[data-diff="normal"].selected { border-color:var(--gold); color:var(--gold); box-shadow:0 0 15px rgba(240,165,0,0.2); } | |
| .diff-btn[data-diff="hard"].selected { border-color:var(--red); color:var(--red); box-shadow:0 0 15px rgba(232,0,29,0.2); } | |
| .connect-panel { | |
| width:100%; max-width:700px; | |
| background: rgba(255,255,255,0.02); border:1px solid var(--border); | |
| padding:24px; | |
| } | |
| .connect-title { font-family:'Oswald',sans-serif; font-size:18px; letter-spacing:4px; color:var(--gold); margin-bottom:16px; } | |
| .connect-row { display:flex; gap:12px; align-items:center; margin:10px 0; } | |
| .connect-input { | |
| flex:1; background:rgba(0,0,0,0.4); border:1px solid var(--border); | |
| color:#fff; padding:10px 14px; | |
| font-family:'Share Tech Mono',monospace; font-size:14px; | |
| outline:none; transition:border 0.2s; | |
| } | |
| .connect-input:focus { border-color:var(--blue); } | |
| .connect-btn { | |
| padding:10px 20px; background:rgba(30,144,255,0.15); | |
| border:1px solid var(--blue); color:var(--blue); | |
| font-family:'Oswald',sans-serif; font-size:14px; letter-spacing:2px; | |
| cursor:pointer; transition:all 0.2s; | |
| } | |
| .connect-btn:hover { background:rgba(30,144,255,0.3); } | |
| .bt-status { | |
| font-family:'Share Tech Mono',monospace; font-size:12px; | |
| color:#666; margin-top:8px; | |
| padding:8px 12px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.05); | |
| } | |
| .bt-dot { display:inline-block; width:8px; height:8px; border-radius:50%; background:#666; margin-right:8px; } | |
| .bt-dot.connected { background:#4ade80; box-shadow:0 0 8px #4ade80; animation:pulse 1s infinite; } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} } | |
| /* βββββββββββββββ GAME CANVAS βββββββββββββββ */ | |
| #game-screen { | |
| flex-direction:column; position:relative; | |
| } | |
| #gameCanvas { | |
| position:absolute; top:0; left:0; | |
| width:100%; height:100%; | |
| } | |
| /* HUD */ | |
| #hud { | |
| position:absolute; inset:0; pointer-events:none; z-index:10; | |
| } | |
| .hud-corner { | |
| position:absolute; padding:16px 20px; | |
| background:rgba(0,0,0,0.6); backdrop-filter:blur(4px); | |
| } | |
| #hud-tl { top:0; left:0; border-right:1px solid rgba(255,255,255,0.06); border-bottom:1px solid rgba(255,255,255,0.06); } | |
| #hud-tr { top:0; right:0; border-left:1px solid rgba(255,255,255,0.06); border-bottom:1px solid rgba(255,255,255,0.06); text-align:right; } | |
| #hud-bl { bottom:0; left:0; border-right:1px solid rgba(255,255,255,0.06); border-top:1px solid rgba(255,255,255,0.06); } | |
| .hud-label { font-size:10px; color:#666; letter-spacing:3px; font-family:'Share Tech Mono',monospace; } | |
| .hud-val { font-family:'Oswald',sans-serif; font-size:26px; color:#fff; line-height:1; } | |
| .hud-val.danger { color:var(--red); animation:blink 0.5s infinite; } | |
| @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.5} } | |
| .health-bar-wrap { margin-top:6px; width:140px; height:6px; background:rgba(255,255,255,0.1); } | |
| .health-bar { height:100%; background:linear-gradient(90deg,#4ade80,var(--gold)); transition:width 0.3s; } | |
| .crosshair { | |
| position:absolute; top:50%; left:50%; | |
| transform:translate(-50%,-50%); | |
| pointer-events:none; | |
| } | |
| .crosshair svg { opacity:0.85; } | |
| #kill-feed { | |
| position:absolute; top:16px; left:50%; transform:translateX(-50%); | |
| display:flex; flex-direction:column; gap:4px; align-items:center; | |
| pointer-events:none; | |
| } | |
| .kill-msg { | |
| background:rgba(0,0,0,0.7); border-left:3px solid var(--red); | |
| padding:4px 12px; font-family:'Share Tech Mono',monospace; | |
| font-size:12px; color:#fff; animation:killSlide 3s forwards; | |
| white-space:nowrap; | |
| } | |
| @keyframes killSlide { 0%{opacity:0;transform:translateY(-10px)} 10%{opacity:1;transform:translateY(0)} 80%{opacity:1} 100%{opacity:0} } | |
| #zone-indicator { | |
| position:absolute; top:80px; right:20px; pointer-events:none; | |
| background:rgba(0,0,0,0.7); border:1px solid var(--red); | |
| padding:8px 14px; | |
| } | |
| .zone-label { font-size:10px; color:var(--red); letter-spacing:3px; font-family:'Share Tech Mono',monospace; } | |
| .zone-timer { font-family:'Oswald',sans-serif; font-size:22px; color:#fff; } | |
| #minimap-wrap { | |
| position:absolute; bottom:20px; right:20px; | |
| width:160px; height:160px; | |
| border:1px solid rgba(255,255,255,0.1); | |
| background:rgba(0,0,0,0.5); overflow:hidden; | |
| } | |
| #minimap { width:100%; height:100%; } | |
| .ammo-display { | |
| font-family:'Oswald',sans-serif; font-size:28px; color:#fff; | |
| } | |
| .ammo-reserve { font-size:14px; color:#666; font-family:'Share Tech Mono',monospace; } | |
| .gun-name-hud { font-family:'Share Tech Mono',monospace; font-size:11px; color:var(--gold); letter-spacing:3px; } | |
| #alive-count { position:absolute; top:60px; right:20px; display:flex; align-items:center; gap:8px; } | |
| .alive-icon { font-size:16px; } | |
| .alive-num { font-family:'Oswald',sans-serif; font-size:22px; color:#fff; } | |
| .alive-label { font-size:10px; color:#666; letter-spacing:2px; font-family:'Share Tech Mono',monospace; } | |
| /* βββββββββββββββ GAME OVER βββββββββββββββ */ | |
| #game-over { | |
| background:rgba(0,0,0,0.92); | |
| flex-direction:column; align-items:center; justify-content:center; | |
| gap:20px; z-index:100; | |
| } | |
| .go-title { | |
| font-family:'Oswald',sans-serif; font-size:80px; | |
| letter-spacing:12px; line-height:1; | |
| } | |
| .go-title.win { color:var(--gold); text-shadow:0 0 40px rgba(240,165,0,0.5); } | |
| .go-title.loss { color:var(--red); text-shadow:0 0 40px var(--glow); } | |
| .go-stats { display:flex; gap:40px; margin:20px 0; } | |
| .go-stat { text-align:center; } | |
| .go-stat-num { font-family:'Oswald',sans-serif; font-size:48px; color:#fff; } | |
| .go-stat-label { font-size:12px; color:#666; letter-spacing:3px; font-family:'Share Tech Mono',monospace; } | |
| /* βββββββββββββββ SHOOTING FLASH βββββββββββββββ */ | |
| #muzzle-flash { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); pointer-events:none; z-index:20; display:none; } | |
| /* βββββββββββββββ DAMAGE INDICATOR βββββββββββββββ */ | |
| #damage-vignette { position:absolute; inset:0; pointer-events:none; z-index:9; transition:opacity 0.3s; opacity:0; | |
| background:radial-gradient(ellipse at center, transparent 40%, rgba(232,0,29,0.6) 100%); } | |
| /* scroll fix */ | |
| ::-webkit-scrollbar { width:4px; } | |
| ::-webkit-scrollbar-track { background:#0a0a0a; } | |
| ::-webkit-scrollbar-thumb { background:var(--red); } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- βββββββββββ MAIN MENU βββββββββββ --> | |
| <div id="main-menu" class="screen active"> | |
| <div class="scanlines"></div> | |
| <div class="menu-left"> | |
| <div class="game-title">WARZONE<br>X</div> | |
| <div class="game-subtitle">β COMBAT REMASTERED β</div> | |
| <div class="divider"></div> | |
| <div class="menu-buttons"> | |
| <button class="menu-btn" onclick="showModeSelect()"><span class="btn-icon">β</span> PLAY</button> | |
| <button class="menu-btn" onclick="showLoadout()"><span class="btn-icon">π«</span> LOADOUT</button> | |
| <button class="menu-btn" style="opacity:0.5;cursor:default"><span class="btn-icon">β</span> SETTINGS</button> | |
| </div> | |
| </div> | |
| <div class="menu-right"> | |
| <div class="char-card"> | |
| <!-- Vivian Harris SVG Character --> | |
| <div class="char-avatar"> | |
| <svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"> | |
| <!-- Body / Outfit --> | |
| <defs> | |
| <radialGradient id="skinGrad" cx="50%" cy="30%" r="60%"> | |
| <stop offset="0%" stop-color="#C68642"/> | |
| <stop offset="100%" stop-color="#8B5E3C"/> | |
| </radialGradient> | |
| <radialGradient id="faceGrad" cx="50%" cy="40%" r="60%"> | |
| <stop offset="0%" stop-color="#D4956A"/> | |
| <stop offset="100%" stop-color="#A0632A"/> | |
| </radialGradient> | |
| </defs> | |
| <!-- Torso / Red Jacket --> | |
| <rect x="42" y="88" width="76" height="58" rx="4" fill="#cc0000"/> | |
| <rect x="42" y="88" width="76" height="58" rx="4" fill="url(#redJacketGrad)" opacity="0.8"/> | |
| <!-- Jacket detail lines --> | |
| <line x1="80" y1="88" x2="80" y2="146" stroke="#990000" stroke-width="2"/> | |
| <rect x="56" y="96" width="12" height="8" rx="2" fill="#ff3333" opacity="0.6"/> | |
| <rect x="92" y="96" width="12" height="8" rx="2" fill="#ff3333" opacity="0.6"/> | |
| <!-- Collar --> | |
| <polygon points="70,88 80,100 90,88" fill="#880000"/> | |
| <!-- Neck --> | |
| <rect x="72" y="78" width="16" height="14" rx="3" fill="url(#faceGrad)"/> | |
| <!-- Head --> | |
| <ellipse cx="80" cy="64" rx="28" ry="30" fill="url(#faceGrad)"/> | |
| <!-- Black Cap --> | |
| <ellipse cx="80" cy="38" rx="30" ry="8" fill="#111"/> | |
| <rect x="52" y="30" width="56" height="18" rx="6" fill="#1a1a1a"/> | |
| <rect x="52" y="30" width="56" height="10" rx="4" fill="#222"/> | |
| <!-- Cap brim --> | |
| <ellipse cx="80" cy="48" rx="32" ry="5" fill="#111" opacity="0.8"/> | |
| <!-- Cap logo --> | |
| <circle cx="80" cy="38" r="6" fill="#e8001d" opacity="0.9"/> | |
| <text x="80" y="42" text-anchor="middle" fill="#fff" font-size="7" font-weight="bold" font-family="Arial">VH</text> | |
| <!-- Eyes --> | |
| <ellipse cx="68" cy="62" rx="5" ry="6" fill="#fff"/> | |
| <ellipse cx="92" cy="62" rx="5" ry="6" fill="#fff"/> | |
| <circle cx="68" cy="63" r="3.5" fill="#3D2000"/> | |
| <circle cx="92" cy="63" r="3.5" fill="#3D2000"/> | |
| <circle cx="69" cy="62" r="1.2" fill="#000"/> | |
| <circle cx="93" cy="62" r="1.2" fill="#000"/> | |
| <!-- Eye shine --> | |
| <circle cx="70" cy="61" r="1" fill="#fff" opacity="0.9"/> | |
| <circle cx="94" cy="61" r="1" fill="#fff" opacity="0.9"/> | |
| <!-- Eyebrows --> | |
| <path d="M63 56 Q68 53 73 56" stroke="#5C3317" stroke-width="2.5" fill="none" stroke-linecap="round"/> | |
| <path d="M87 56 Q92 53 97 56" stroke="#5C3317" stroke-width="2.5" fill="none" stroke-linecap="round"/> | |
| <!-- Nose --> | |
| <path d="M78 67 Q80 72 82 67" stroke="#8B5E3C" stroke-width="1.5" fill="none"/> | |
| <!-- Smile --> | |
| <path d="M72 76 Q80 82 88 76" stroke="#7A3A1A" stroke-width="2" fill="none" stroke-linecap="round"/> | |
| <!-- Lips --> | |
| <path d="M73 77 Q80 81 87 77" stroke="#c0392b" stroke-width="2.5" fill="none" stroke-linecap="round"/> | |
| <!-- Hair strands --> | |
| <path d="M52 50 Q48 62 50 76" stroke="#3D2000" stroke-width="4" fill="none" stroke-linecap="round"/> | |
| <path d="M108 50 Q112 62 110 76" stroke="#3D2000" stroke-width="4" fill="none" stroke-linecap="round"/> | |
| <!-- Arms --> | |
| <rect x="22" y="88" width="20" height="50" rx="8" fill="#cc0000"/> | |
| <rect x="118" y="88" width="20" height="50" rx="8" fill="#cc0000"/> | |
| <!-- Hands --> | |
| <ellipse cx="32" cy="140" rx="10" ry="8" fill="url(#faceGrad)"/> | |
| <ellipse cx="128" cy="140" rx="10" ry="8" fill="url(#faceGrad)"/> | |
| <!-- Gun in right hand --> | |
| <rect x="130" y="128" width="22" height="8" rx="2" fill="#333"/> | |
| <rect x="136" y="120" width="8" height="12" rx="1" fill="#444"/> | |
| <!-- Boots --> | |
| <rect x="52" y="142" width="22" height="16" rx="4" fill="#1a1a1a"/> | |
| <rect x="86" y="142" width="22" height="16" rx="4" fill="#1a1a1a"/> | |
| <!-- Tactical belt --> | |
| <rect x="42" y="114" width="76" height="8" rx="2" fill="#880000"/> | |
| <rect x="76" y="113" width="8" height="10" rx="1" fill="#cc8800"/> | |
| </svg> | |
| </div> | |
| <div class="char-name">VIVIAN HARRIS</div> | |
| <div class="char-rank">β ELITE OPERATOR β</div> | |
| <div class="char-stats"> | |
| <div class="stat-item"><div class="stat-label">KILLS</div><div class="stat-val">2,847</div></div> | |
| <div class="stat-item"><div class="stat-label">K/D RATIO</div><div class="stat-val">4.2</div></div> | |
| <div class="stat-item"><div class="stat-label">WINS</div><div class="stat-val">183</div></div> | |
| <div class="stat-item"><div class="stat-label">RANK</div><div class="stat-val">117</div></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- βββββββββββ MODE SELECT βββββββββββ --> | |
| <div id="mode-select" class="screen"> | |
| <div class="mode-title">β SELECT MODE β</div> | |
| <div class="mode-cards"> | |
| <div class="mode-card" onclick="showLoadout()"> | |
| <div class="mode-badge">PVP</div> | |
| <div class="mode-card-img">π―</div> | |
| <div class="mode-card-body"> | |
| <div class="mode-card-name">MULTIPLAYER</div> | |
| <div class="mode-card-desc">Classic team deathmatch. Choose your loadout and dominate the battlefield against AI opponents.</div> | |
| </div> | |
| </div> | |
| <div class="mode-card" onclick="showBRLobby()"> | |
| <div class="mode-badge">SOLO</div> | |
| <div class="mode-card-img">πΊ</div> | |
| <div class="mode-card-body"> | |
| <div class="mode-card-name">BATTLE ROYALE</div> | |
| <div class="mode-card-desc">20 players, one survivor. Shrinking zone forces close combat. Last one standing wins.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="back-btn" onclick="showScreen('main-menu')">β BACK</button> | |
| </div> | |
| <!-- βββββββββββ LOADOUT βββββββββββ --> | |
| <div id="loadout-screen" class="screen"> | |
| <div class="screen-header"> | |
| <button class="back-btn" onclick="showScreen('mode-select')">β BACK</button> | |
| <div class="screen-title">LOADOUT</div> | |
| </div> | |
| <div class="loadout-grid"> | |
| <!-- M117 --> | |
| <div class="gun-card" id="gun-117" onclick="selectGun('117')"> | |
| <div class="gun-type">ASSAULT RIFLE</div> | |
| <div class="gun-name">M117</div> | |
| <div class="gun-art">π«</div> | |
| <div class="gun-stats"> | |
| <div class="gun-stat"> | |
| <div class="gun-stat-label">DAMAGE</div> | |
| <div class="gun-stat-bar"><div class="gun-stat-fill" style="width:75%"></div></div> | |
| </div> | |
| <div class="gun-stat"> | |
| <div class="gun-stat-label">FIRE RATE</div> | |
| <div class="gun-stat-bar"><div class="gun-stat-fill" style="width:88%"></div></div> | |
| </div> | |
| <div class="gun-stat"> | |
| <div class="gun-stat-label">ACCURACY</div> | |
| <div class="gun-stat-bar"><div class="gun-stat-fill" style="width:65%"></div></div> | |
| </div> | |
| <div class="gun-stat"> | |
| <div class="gun-stat-label">RANGE</div> | |
| <div class="gun-stat-bar"><div class="gun-stat-fill" style="width:80%"></div></div> | |
| </div> | |
| </div> | |
| <div class="gun-select-indicator">β SELECTED</div> | |
| </div> | |
| <!-- M4 --> | |
| <div class="gun-card" id="gun-m4" onclick="selectGun('m4')"> | |
| <div class="gun-type">CARBINE RIFLE</div> | |
| <div class="gun-name">M4A1</div> | |
| <div class="gun-art">π«</div> | |
| <div class="gun-stats"> | |
| <div class="gun-stat"> | |
| <div class="gun-stat-label">DAMAGE</div> | |
| <div class="gun-stat-bar"><div class="gun-stat-fill" style="width:60%"></div></div> | |
| </div> | |
| <div class="gun-stat"> | |
| <div class="gun-stat-label">FIRE RATE</div> | |
| <div class="gun-stat-bar"><div class="gun-stat-fill" style="width:95%"></div></div> | |
| </div> | |
| <div class="gun-stat"> | |
| <div class="gun-stat-label">ACCURACY</div> | |
| <div class="gun-stat-bar"><div class="gun-stat-fill" style="width:82%"></div></div> | |
| </div> | |
| <div class="gun-stat"> | |
| <div class="gun-stat-label">RANGE</div> | |
| <div class="gun-stat-bar"><div class="gun-stat-fill" style="width:55%"></div></div> | |
| </div> | |
| </div> | |
| <div class="gun-select-indicator">β SELECTED</div> | |
| </div> | |
| </div> | |
| <div class="loadout-confirm"> | |
| <button class="confirm-btn" id="loadout-confirm-btn" onclick="confirmLoadout()" disabled>SELECT A WEAPON TO CONTINUE</button> | |
| </div> | |
| </div> | |
| <!-- βββββββββββ BATTLE ROYALE LOBBY βββββββββββ --> | |
| <div id="br-lobby" class="screen"> | |
| <div class="screen-header"> | |
| <button class="back-btn" onclick="showScreen('mode-select')">β BACK</button> | |
| <div class="screen-title">BATTLE ROYALE</div> | |
| </div> | |
| <div style="font-size:12px;color:#666;letter-spacing:3px;font-family:'Share Tech Mono',monospace;">β SELECT DIFFICULTY β</div> | |
| <div class="diff-grid"> | |
| <button class="diff-btn" data-diff="noob" onclick="selectDiff('noob')">NOOB</button> | |
| <button class="diff-btn" data-diff="easy" onclick="selectDiff('easy')">EASY</button> | |
| <button class="diff-btn" data-diff="normal" onclick="selectDiff('normal')">NORMAL</button> | |
| <button class="diff-btn" data-diff="hard" onclick="selectDiff('hard')">HARD</button> | |
| </div> | |
| <div style="font-size:12px;color:#666;letter-spacing:3px;font-family:'Share Tech Mono',monospace;margin-top:8px;">β CONNECTIVITY β</div> | |
| <div class="connect-panel"> | |
| <div class="connect-title">π΅ BLUETOOTH / LAN CONNECT</div> | |
| <p style="font-size:13px;color:#666;margin-bottom:14px;line-height:1.6;">Connect with a friend on the same network or Bluetooth. Enter their code to join their session.</p> | |
| <div class="connect-row"> | |
| <input class="connect-input" placeholder="YOUR ROOM CODE: VH-2024" id="room-code" readonly value="VH-4829"> | |
| <button class="connect-btn" onclick="copyCode()">COPY</button> | |
| </div> | |
| <div class="connect-row"> | |
| <input class="connect-input" placeholder="ENTER FRIEND'S CODE..." id="friend-code" maxlength="7"> | |
| <button class="connect-btn" onclick="tryConnect()">JOIN</button> | |
| </div> | |
| <div class="bt-status"> | |
| <span class="bt-dot" id="bt-dot"></span> | |
| <span id="bt-status-text">SEARCHING FOR DEVICES...</span> | |
| </div> | |
| </div> | |
| <div class="loadout-confirm" style="max-width:700px;margin-top:10px;"> | |
| <button class="confirm-btn" id="br-start-btn" onclick="startBR()" disabled>SELECT DIFFICULTY TO DEPLOY</button> | |
| </div> | |
| </div> | |
| <!-- βββββββββββ GAME SCREEN βββββββββββ --> | |
| <div id="game-screen" class="screen"> | |
| <canvas id="gameCanvas"></canvas> | |
| <div id="damage-vignette"></div> | |
| <div id="muzzle-flash"> | |
| <svg width="60" height="60"><circle cx="30" cy="30" r="25" fill="rgba(255,200,50,0.9)"/><circle cx="30" cy="30" r="15" fill="#fff"/></svg> | |
| </div> | |
| <div id="hud"> | |
| <!-- Top Left: Health --> | |
| <div class="hud-corner" id="hud-tl"> | |
| <div class="hud-label">HEALTH</div> | |
| <div class="hud-val" id="hud-health">100</div> | |
| <div class="health-bar-wrap"><div class="health-bar" id="health-bar" style="width:100%"></div></div> | |
| </div> | |
| <!-- Top Right: Kills + Alive --> | |
| <div class="hud-corner" id="hud-tr"> | |
| <div class="hud-label">KILLS</div> | |
| <div class="hud-val" id="hud-kills">0</div> | |
| </div> | |
| <!-- Alive count --> | |
| <div id="alive-count"> | |
| <span class="alive-icon">π€</span> | |
| <div> | |
| <div class="alive-num" id="hud-alive">20</div> | |
| <div class="alive-label">ALIVE</div> | |
| </div> | |
| </div> | |
| <!-- Bottom Left: Gun + Ammo --> | |
| <div class="hud-corner" id="hud-bl"> | |
| <div class="gun-name-hud" id="hud-gun">M117</div> | |
| <div class="ammo-display"><span id="hud-ammo">30</span></div> | |
| <div class="ammo-reserve" id="hud-reserve">/ 120</div> | |
| </div> | |
| <!-- Zone Timer --> | |
| <div id="zone-indicator"> | |
| <div class="zone-label">⬑ ZONE CLOSING</div> | |
| <div class="zone-timer" id="zone-timer">60</div> | |
| </div> | |
| <!-- Kill Feed --> | |
| <div id="kill-feed"></div> | |
| <!-- Crosshair --> | |
| <div class="crosshair"> | |
| <svg width="60" height="60" viewBox="0 0 60 60"> | |
| <line x1="30" y1="5" x2="30" y2="22" stroke="white" stroke-width="1.5" opacity="0.8"/> | |
| <line x1="30" y1="38" x2="30" y2="55" stroke="white" stroke-width="1.5" opacity="0.8"/> | |
| <line x1="5" y1="30" x2="22" y2="30" stroke="white" stroke-width="1.5" opacity="0.8"/> | |
| <line x1="38" y1="30" x2="55" y2="30" stroke="white" stroke-width="1.5" opacity="0.8"/> | |
| <circle cx="30" cy="30" r="3" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1"/> | |
| </svg> | |
| </div> | |
| <!-- Minimap --> | |
| <div id="minimap-wrap"> | |
| <canvas id="minimap" width="160" height="160"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- βββββββββββ GAME OVER βββββββββββ --> | |
| <div id="game-over" class="screen"> | |
| <div class="go-title" id="go-title">ELIMINATED</div> | |
| <div style="font-size:14px;color:#666;letter-spacing:4px;font-family:'Share Tech Mono',monospace" id="go-sub">BETTER LUCK NEXT TIME, HARRIS</div> | |
| <div class="go-stats"> | |
| <div class="go-stat"><div class="go-stat-num" id="go-kills">0</div><div class="go-stat-label">KILLS</div></div> | |
| <div class="go-stat"><div class="go-stat-num" id="go-place">20</div><div class="go-stat-label">PLACEMENT</div></div> | |
| <div class="go-stat"><div class="go-stat-num" id="go-time">0:00</div><div class="go-stat-label">SURVIVED</div></div> | |
| </div> | |
| <button class="confirm-btn" style="max-width:400px" onclick="backToMenu()">β RETURN TO MENU</button> | |
| </div> | |
| <script> | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // STATE | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let selectedGun = null; | |
| let selectedDiff = null; | |
| let gameState = {}; | |
| let gameLoop = null; | |
| let gameStartTime = 0; | |
| const GUNS = { | |
| '117': { name:'M117', damage:35, fireRate:150, ammo:30, reserve:120, color:'#f0a500' }, | |
| 'm4': { name:'M4A1', damage:25, fireRate:80, ammo:30, reserve:150, color:'#1e90ff' } | |
| }; | |
| const DIFF_CONFIG = { | |
| noob: { botSpeed:0.3, botAccuracy:0.05, botReaction:3000, label:'NOOB' }, | |
| easy: { botSpeed:0.6, botAccuracy:0.12, botReaction:2000, label:'EASY' }, | |
| normal: { botSpeed:1.0, botAccuracy:0.22, botReaction:1200, label:'NORMAL' }, | |
| hard: { botSpeed:1.5, botAccuracy:0.38, botReaction:600, label:'HARD' } | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // NAVIGATION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showScreen(id) { | |
| document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); | |
| document.getElementById(id).classList.add('active'); | |
| } | |
| function showModeSelect() { showScreen('mode-select'); } | |
| function showLoadout() { showScreen('loadout-screen'); } | |
| function showBRLobby() { showScreen('br-lobby'); simulateBluetooth(); } | |
| function backToMenu() { stopGame(); showScreen('main-menu'); } | |
| function selectGun(g) { | |
| selectedGun = g; | |
| document.querySelectorAll('.gun-card').forEach(c => c.classList.remove('selected')); | |
| document.getElementById('gun-' + g).classList.add('selected'); | |
| const btn = document.getElementById('loadout-confirm-btn'); | |
| btn.disabled = false; | |
| btn.textContent = 'β‘ DEPLOY WITH ' + GUNS[g].name; | |
| } | |
| function confirmLoadout() { | |
| if (!selectedGun) return; | |
| startGame(false); | |
| } | |
| function selectDiff(d) { | |
| selectedDiff = d; | |
| document.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('selected')); | |
| document.querySelector('[data-diff="' + d + '"]').classList.add('selected'); | |
| const btn = document.getElementById('br-start-btn'); | |
| btn.disabled = false; | |
| btn.textContent = 'β‘ DEPLOY TO BATTLE ROYALE β ' + DIFF_CONFIG[d].label; | |
| } | |
| function copyCode() { | |
| const v = document.getElementById('room-code').value; | |
| navigator.clipboard?.writeText(v); | |
| alert('Room code copied: ' + v); | |
| } | |
| function tryConnect() { | |
| const code = document.getElementById('friend-code').value.trim(); | |
| if (!code) { alert('Enter a friend\'s room code first!'); return; } | |
| const dot = document.getElementById('bt-dot'); | |
| const txt = document.getElementById('bt-status-text'); | |
| txt.textContent = 'CONNECTING TO ' + code + '...'; | |
| setTimeout(() => { | |
| dot.classList.add('connected'); | |
| txt.textContent = 'CONNECTED β FRIEND JOINED SESSION'; | |
| }, 1800); | |
| } | |
| function simulateBluetooth() { | |
| const dot = document.getElementById('bt-dot'); | |
| const txt = document.getElementById('bt-status-text'); | |
| dot.classList.remove('connected'); | |
| txt.textContent = 'SCANNING FOR NEARBY DEVICES...'; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // GAME ENGINE | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const minimap = document.getElementById('minimap'); | |
| const mctx = minimap.getContext('2d'); | |
| function startGame(isMultiplayer) { | |
| // Default gun if none selected | |
| if (!selectedGun) selectedGun = '117'; | |
| if (!selectedDiff) selectedDiff = 'normal'; | |
| showScreen('game-screen'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| const gun = GUNS[selectedGun]; | |
| const diff = DIFF_CONFIG[selectedDiff]; | |
| gameStartTime = Date.now(); | |
| // Map | |
| const MAP_W = 2400, MAP_H = 2400; | |
| // Buildings / obstacles | |
| const buildings = []; | |
| for (let i = 0; i < 40; i++) { | |
| buildings.push({ | |
| x: 100 + Math.random() * (MAP_W - 200), | |
| y: 100 + Math.random() * (MAP_H - 200), | |
| w: 60 + Math.random() * 120, | |
| h: 60 + Math.random() * 120, | |
| color: `hsl(${200 + Math.random()*40}, 20%, ${12 + Math.random()*10}%)` | |
| }); | |
| } | |
| // Trees | |
| const trees = []; | |
| for (let i = 0; i < 80; i++) { | |
| trees.push({ | |
| x: Math.random() * MAP_W, | |
| y: Math.random() * MAP_H, | |
| r: 20 + Math.random() * 25 | |
| }); | |
| } | |
| // Player | |
| const player = { | |
| x: MAP_W / 2, y: MAP_H / 2, | |
| w: 22, h: 22, | |
| hp: 100, maxHp: 100, | |
| speed: 3, | |
| angle: 0, | |
| kills: 0, | |
| ammo: gun.ammo, | |
| reserve: gun.reserve, | |
| lastShot: 0, | |
| alive: true | |
| }; | |
| // Camera | |
| const cam = { x: 0, y: 0 }; | |
| // Bots | |
| const bots = []; | |
| for (let i = 0; i < 20; i++) { | |
| let bx, by; | |
| do { | |
| bx = 80 + Math.random() * (MAP_W - 160); | |
| by = 80 + Math.random() * (MAP_H - 160); | |
| } while (Math.hypot(bx - player.x, by - player.y) < 200); | |
| bots.push({ | |
| x: bx, y: by, w: 20, h: 20, | |
| hp: 60 + Math.random() * 40, | |
| maxHp: 100, | |
| speed: diff.botSpeed * (0.7 + Math.random() * 0.6), | |
| angle: Math.random() * Math.PI * 2, | |
| alive: true, | |
| lastShot: 0, | |
| shootInterval: diff.botReaction + Math.random() * 1000, | |
| wanderTimer: 0, | |
| wanderAngle: Math.random() * Math.PI * 2, | |
| hue: Math.floor(Math.random() * 360), | |
| name: ['WOLF','VIPER','GHOST','REAPER','SHADE','ROGUE','BLADE','STORM','HAWK','SKULL', | |
| 'DEMON','COBRA','TITAN','RAMBO','NINJA','DEATH','FLASH','CHAOS','OMEGA','ZERO'][i] | |
| }); | |
| } | |
| // Bullets | |
| const bullets = []; | |
| const botBullets = []; | |
| // Zone | |
| let zoneRadius = Math.min(MAP_W, MAP_H) * 0.48; | |
| const zoneX = MAP_W / 2, zoneY = MAP_H / 2; | |
| let zoneShrinking = false; | |
| let zoneTimer = 60; | |
| let zoneInterval = null; | |
| // Keys | |
| const keys = {}; | |
| document.addEventListener('keydown', e => keys[e.key.toLowerCase()] = true); | |
| document.addEventListener('keyup', e => keys[e.key.toLowerCase()] = false); | |
| // Mouse | |
| let mouseX = canvas.width / 2, mouseY = canvas.height / 2; | |
| canvas.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; }); | |
| canvas.addEventListener('click', shoot); | |
| function shoot() { | |
| if (!player.alive) return; | |
| const now = Date.now(); | |
| if (now - player.lastShot < gun.fireRate) return; | |
| if (player.ammo <= 0) { | |
| if (player.reserve > 0) { player.ammo = Math.min(gun.ammo, player.reserve); player.reserve -= player.ammo; } | |
| return; | |
| } | |
| player.lastShot = now; | |
| player.ammo--; | |
| // Muzzle flash | |
| const mf = document.getElementById('muzzle-flash'); | |
| mf.style.display = 'block'; | |
| setTimeout(() => mf.style.display = 'none', 60); | |
| const angle = Math.atan2(mouseY - canvas.height/2, mouseX - canvas.width/2); | |
| const spread = selectedGun === 'm4' ? 0.05 : 0.08; | |
| bullets.push({ | |
| x: player.x, y: player.y, | |
| vx: Math.cos(angle + (Math.random()-0.5)*spread) * 18, | |
| vy: Math.sin(angle + (Math.random()-0.5)*spread) * 18, | |
| life: 80, damage: gun.damage | |
| }); | |
| } | |
| // Zone countdown | |
| function startZoneTimer() { | |
| zoneTimer = 60; | |
| zoneShrinking = false; | |
| clearInterval(zoneInterval); | |
| zoneInterval = setInterval(() => { | |
| zoneTimer--; | |
| document.getElementById('zone-timer').textContent = zoneTimer; | |
| if (zoneTimer <= 0) { | |
| zoneShrinking = true; | |
| zoneTimer = 30; | |
| } | |
| if (zoneShrinking && zoneRadius > 80) zoneRadius -= 4; | |
| }, 1000); | |
| } | |
| startZoneTimer(); | |
| function addKillFeed(msg) { | |
| const feed = document.getElementById('kill-feed'); | |
| const el = document.createElement('div'); | |
| el.className = 'kill-msg'; | |
| el.textContent = msg; | |
| feed.appendChild(el); | |
| setTimeout(() => el.remove(), 3000); | |
| } | |
| let frameId; | |
| function update() { | |
| // Player movement | |
| if (player.alive) { | |
| let dx = 0, dy = 0; | |
| if (keys['w'] || keys['arrowup']) dy = -1; | |
| if (keys['s'] || keys['arrowdown']) dy = 1; | |
| if (keys['a'] || keys['arrowleft']) dx = -1; | |
| if (keys['d'] || keys['arrowright']) dx = 1; | |
| if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707; } | |
| const nx = player.x + dx * player.speed; | |
| const ny = player.y + dy * player.speed; | |
| // Boundary | |
| if (nx > 10 && nx < MAP_W - 10) player.x = nx; | |
| if (ny > 10 && ny < MAP_H - 10) player.y = ny; | |
| // Zone damage | |
| const distZone = Math.hypot(player.x - zoneX, player.y - zoneY); | |
| if (distZone > zoneRadius) { | |
| player.hp -= 0.15; | |
| showDamage(); | |
| } | |
| } | |
| // Camera follow | |
| cam.x = player.x - canvas.width / 2; | |
| cam.y = player.y - canvas.height / 2; | |
| // Player angle toward mouse | |
| player.angle = Math.atan2(mouseY - canvas.height/2, mouseX - canvas.width/2); | |
| // Bullets | |
| for (let i = bullets.length - 1; i >= 0; i--) { | |
| const b = bullets[i]; | |
| b.x += b.vx; b.y += b.vy; b.life--; | |
| if (b.life <= 0) { bullets.splice(i, 1); continue; } | |
| // Hit bots | |
| for (const bot of bots) { | |
| if (!bot.alive) continue; | |
| if (Math.abs(b.x - bot.x) < bot.w && Math.abs(b.y - bot.y) < bot.h) { | |
| bot.hp -= b.damage; | |
| bullets.splice(i, 1); | |
| if (bot.hp <= 0) { | |
| bot.alive = false; | |
| player.kills++; | |
| addKillFeed('π― VIVIAN HARRIS β ' + bot.name); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| // Bot AI | |
| const now = Date.now(); | |
| for (const bot of bots) { | |
| if (!bot.alive) continue; | |
| const dx = player.x - bot.x; | |
| const dy = player.y - bot.y; | |
| const dist = Math.hypot(dx, dy); | |
| // Zone damage on bots | |
| const bDist = Math.hypot(bot.x - zoneX, bot.y - zoneY); | |
| if (bDist > zoneRadius) { bot.x += (zoneX - bot.x) * 0.01; bot.y += (zoneY - bot.y) * 0.01; } | |
| if (dist < 600) { | |
| // Chase player | |
| bot.angle = Math.atan2(dy, dx); | |
| bot.x += Math.cos(bot.angle) * bot.speed; | |
| bot.y += Math.sin(bot.angle) * bot.speed; | |
| // Shoot at player | |
| if (dist < 400 && now - bot.lastShot > bot.shootInterval && player.alive) { | |
| bot.lastShot = now; | |
| const spread = 1 - diff.botAccuracy; | |
| const angle = bot.angle + (Math.random() - 0.5) * spread * 1.5; | |
| botBullets.push({ | |
| x: bot.x, y: bot.y, | |
| vx: Math.cos(angle) * 12, | |
| vy: Math.sin(angle) * 12, | |
| life: 60, damage: 8 + Math.random() * 8 | |
| }); | |
| } | |
| } else { | |
| // Wander | |
| bot.wanderTimer--; | |
| if (bot.wanderTimer <= 0) { | |
| bot.wanderAngle = Math.random() * Math.PI * 2; | |
| bot.wanderTimer = 60 + Math.random() * 120; | |
| } | |
| bot.x += Math.cos(bot.wanderAngle) * bot.speed * 0.5; | |
| bot.y += Math.sin(bot.wanderAngle) * bot.speed * 0.5; | |
| bot.x = Math.max(10, Math.min(MAP_W-10, bot.x)); | |
| bot.y = Math.max(10, Math.min(MAP_H-10, bot.y)); | |
| } | |
| } | |
| // Bot bullets | |
| for (let i = botBullets.length - 1; i >= 0; i--) { | |
| const b = botBullets[i]; | |
| b.x += b.vx; b.y += b.vy; b.life--; | |
| if (b.life <= 0) { botBullets.splice(i, 1); continue; } | |
| if (player.alive && Math.abs(b.x - player.x) < 14 && Math.abs(b.y - player.y) < 14) { | |
| player.hp -= b.damage; | |
| botBullets.splice(i, 1); | |
| showDamage(); | |
| if (player.hp <= 0) { player.alive = false; endGame(false); return; } | |
| } | |
| } | |
| // Check win | |
| const aliveCount = bots.filter(b => b.alive).length; | |
| if (aliveCount === 0 && player.alive) { endGame(true); return; } | |
| // Update HUD | |
| const hp = Math.max(0, Math.round(player.hp)); | |
| document.getElementById('hud-health').textContent = hp; | |
| document.getElementById('hud-health').className = 'hud-val' + (hp < 30 ? ' danger' : ''); | |
| document.getElementById('health-bar').style.width = hp + '%'; | |
| document.getElementById('health-bar').style.background = hp > 60 ? 'linear-gradient(90deg,#4ade80,#22c55e)' : hp > 30 ? 'linear-gradient(90deg,#f0a500,#eab308)' : 'linear-gradient(90deg,#e8001d,#ff4444)'; | |
| document.getElementById('hud-kills').textContent = player.kills; | |
| document.getElementById('hud-alive').textContent = aliveCount + 1; | |
| document.getElementById('hud-ammo').textContent = player.ammo; | |
| document.getElementById('hud-reserve').textContent = '/ ' + player.reserve; | |
| document.getElementById('hud-gun').textContent = gun.name; | |
| } | |
| let damageTimer = null; | |
| function showDamage() { | |
| document.getElementById('damage-vignette').style.opacity = '1'; | |
| clearTimeout(damageTimer); | |
| damageTimer = setTimeout(() => document.getElementById('damage-vignette').style.opacity = '0', 300); | |
| } | |
| // βββ DRAW βββ | |
| function draw() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.save(); | |
| ctx.translate(-cam.x, -cam.y); | |
| // Ground | |
| ctx.fillStyle = '#1a2210'; | |
| ctx.fillRect(0, 0, MAP_W, MAP_H); | |
| // Ground texture grid | |
| ctx.strokeStyle = 'rgba(255,255,255,0.025)'; | |
| ctx.lineWidth = 1; | |
| for (let gx = 0; gx <= MAP_W; gx += 80) { | |
| ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, MAP_H); ctx.stroke(); | |
| } | |
| for (let gy = 0; gy <= MAP_H; gy += 80) { | |
| ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(MAP_W, gy); ctx.stroke(); | |
| } | |
| // Roads | |
| ctx.fillStyle = '#222c1a'; | |
| ctx.fillRect(MAP_W/2 - 30, 0, 60, MAP_H); | |
| ctx.fillRect(0, MAP_H/2 - 30, MAP_W, 60); | |
| ctx.strokeStyle = 'rgba(255,255,100,0.15)'; | |
| ctx.setLineDash([40, 30]); | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); ctx.moveTo(MAP_W/2, 0); ctx.lineTo(MAP_W/2, MAP_H); ctx.stroke(); | |
| ctx.beginPath(); ctx.moveTo(0, MAP_H/2); ctx.lineTo(MAP_W, MAP_H/2); ctx.stroke(); | |
| ctx.setLineDash([]); | |
| // Trees | |
| for (const t of trees) { | |
| ctx.fillStyle = '#1a3a0a'; | |
| ctx.beginPath(); ctx.arc(t.x, t.y, t.r, 0, Math.PI*2); ctx.fill(); | |
| ctx.fillStyle = '#234a10'; | |
| ctx.beginPath(); ctx.arc(t.x - t.r*0.2, t.y - t.r*0.2, t.r*0.6, 0, Math.PI*2); ctx.fill(); | |
| } | |
| // Buildings | |
| for (const b of buildings) { | |
| ctx.fillStyle = b.color; | |
| ctx.fillRect(b.x, b.y, b.w, b.h); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.08)'; | |
| ctx.lineWidth = 1; | |
| ctx.strokeRect(b.x, b.y, b.w, b.h); | |
| // Windows | |
| ctx.fillStyle = 'rgba(255,220,100,0.15)'; | |
| for (let wx = b.x + 10; wx < b.x + b.w - 10; wx += 20) { | |
| for (let wy = b.y + 10; wy < b.y + b.h - 10; wy += 20) { | |
| ctx.fillRect(wx, wy, 8, 8); | |
| } | |
| } | |
| } | |
| // Zone circle | |
| ctx.beginPath(); | |
| ctx.arc(zoneX, zoneY, zoneRadius, 0, Math.PI*2); | |
| ctx.strokeStyle = 'rgba(100,180,255,0.5)'; | |
| ctx.lineWidth = 3; | |
| ctx.stroke(); | |
| // Zone outside fill | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.rect(0, 0, MAP_W, MAP_H); | |
| ctx.arc(zoneX, zoneY, zoneRadius, 0, Math.PI*2, true); | |
| ctx.fillStyle = 'rgba(0,50,150,0.18)'; | |
| ctx.fill(); | |
| ctx.restore(); | |
| // Bot bullets | |
| for (const b of botBullets) { | |
| ctx.fillStyle = '#ff6644'; | |
| ctx.beginPath(); ctx.arc(b.x, b.y, 4, 0, Math.PI*2); ctx.fill(); | |
| ctx.fillStyle = 'rgba(255,100,50,0.3)'; | |
| ctx.beginPath(); ctx.arc(b.x, b.y, 7, 0, Math.PI*2); ctx.fill(); | |
| } | |
| // Player bullets | |
| for (const b of bullets) { | |
| ctx.fillStyle = '#ffe055'; | |
| ctx.beginPath(); ctx.arc(b.x, b.y, 5, 0, Math.PI*2); ctx.fill(); | |
| ctx.fillStyle = 'rgba(255,220,50,0.3)'; | |
| ctx.beginPath(); ctx.arc(b.x, b.y, 9, 0, Math.PI*2); ctx.fill(); | |
| } | |
| // Bots | |
| for (const bot of bots) { | |
| if (!bot.alive) continue; | |
| ctx.save(); | |
| ctx.translate(bot.x, bot.y); | |
| ctx.rotate(bot.angle); | |
| // Bot body | |
| ctx.fillStyle = `hsl(${bot.hue},50%,35%)`; | |
| ctx.fillRect(-bot.w/2, -bot.h/2, bot.w, bot.h); | |
| ctx.strokeStyle = `hsl(${bot.hue},70%,55%)`; | |
| ctx.lineWidth = 1.5; | |
| ctx.strokeRect(-bot.w/2, -bot.h/2, bot.w, bot.h); | |
| // Face dot | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); ctx.arc(5, 0, 3, 0, Math.PI*2); ctx.fill(); | |
| ctx.restore(); | |
| // HP bar | |
| const hpPct = bot.hp / bot.maxHp; | |
| ctx.fillStyle = '#333'; | |
| ctx.fillRect(bot.x - 16, bot.y - bot.h/2 - 10, 32, 4); | |
| ctx.fillStyle = hpPct > 0.5 ? '#4ade80' : hpPct > 0.25 ? '#f0a500' : '#e8001d'; | |
| ctx.fillRect(bot.x - 16, bot.y - bot.h/2 - 10, 32 * hpPct, 4); | |
| // Name tag | |
| ctx.fillStyle = 'rgba(255,255,255,0.7)'; | |
| ctx.font = '9px Share Tech Mono, monospace'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(bot.name, bot.x, bot.y - bot.h/2 - 14); | |
| } | |
| // Player | |
| if (player.alive) { | |
| ctx.save(); | |
| ctx.translate(player.x, player.y); | |
| ctx.rotate(player.angle); | |
| // Shadow | |
| ctx.fillStyle = 'rgba(0,0,0,0.3)'; | |
| ctx.beginPath(); ctx.ellipse(3, 3, 14, 12, 0, 0, Math.PI*2); ctx.fill(); | |
| // Body | |
| ctx.fillStyle = '#cc0000'; | |
| ctx.fillRect(-player.w/2, -player.h/2, player.w, player.h); | |
| // Black cap indicator | |
| ctx.fillStyle = '#111'; | |
| ctx.fillRect(-player.w/2, -player.h/2, player.w, player.h/3); | |
| // Gun | |
| ctx.fillStyle = '#555'; | |
| ctx.fillRect(4, -3, 18, 6); | |
| ctx.fillStyle = '#333'; | |
| ctx.fillRect(18, -2, 6, 4); | |
| // Direction dot | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); ctx.arc(8, 0, 2.5, 0, Math.PI*2); ctx.fill(); | |
| ctx.restore(); | |
| } | |
| ctx.restore(); // un-translate | |
| // βββ MINIMAP βββ | |
| mctx.fillStyle = '#0a0c10'; | |
| mctx.fillRect(0, 0, 160, 160); | |
| const scale = 160 / MAP_W; | |
| // Zone on minimap | |
| mctx.beginPath(); | |
| mctx.arc(zoneX*scale, zoneY*scale, zoneRadius*scale, 0, Math.PI*2); | |
| mctx.strokeStyle = 'rgba(100,180,255,0.6)'; mctx.lineWidth = 1.5; mctx.stroke(); | |
| // Buildings on minimap | |
| mctx.fillStyle = 'rgba(255,255,255,0.08)'; | |
| for (const b of buildings) mctx.fillRect(b.x*scale, b.y*scale, b.w*scale, b.h*scale); | |
| // Bots on minimap | |
| for (const bot of bots) { | |
| if (!bot.alive) continue; | |
| mctx.fillStyle = '#e8001d'; | |
| mctx.beginPath(); mctx.arc(bot.x*scale, bot.y*scale, 3, 0, Math.PI*2); mctx.fill(); | |
| } | |
| // Player on minimap | |
| mctx.fillStyle = '#4ade80'; | |
| mctx.beginPath(); mctx.arc(player.x*scale, player.y*scale, 4, 0, Math.PI*2); mctx.fill(); | |
| mctx.strokeStyle = '#fff'; mctx.lineWidth = 1.5; mctx.stroke(); | |
| } | |
| function gameFrame() { | |
| update(); | |
| draw(); | |
| frameId = requestAnimationFrame(gameFrame); | |
| } | |
| gameLoop = { stop: () => { cancelAnimationFrame(frameId); clearInterval(zoneInterval); } }; | |
| frameId = requestAnimationFrame(gameFrame); | |
| // End game | |
| function endGame(win) { | |
| cancelAnimationFrame(frameId); | |
| clearInterval(zoneInterval); | |
| const survived = Math.round((Date.now() - gameStartTime) / 1000); | |
| const mins = Math.floor(survived / 60); | |
| const secs = survived % 60; | |
| const title = document.getElementById('go-title'); | |
| const sub = document.getElementById('go-sub'); | |
| if (win) { | |
| title.textContent = 'VICTORY!'; | |
| title.className = 'go-title win'; | |
| sub.textContent = 'π VIVIAN HARRIS β WINNER WINNER'; | |
| } else { | |
| title.textContent = 'ELIMINATED'; | |
| title.className = 'go-title loss'; | |
| sub.textContent = 'BETTER LUCK NEXT TIME, HARRIS'; | |
| } | |
| const aliveLeft = bots.filter(b => b.alive).length; | |
| document.getElementById('go-kills').textContent = player.kills; | |
| document.getElementById('go-place').textContent = win ? '#1' : '#' + (aliveLeft + 2); | |
| document.getElementById('go-time').textContent = mins + ':' + String(secs).padStart(2,'0'); | |
| showScreen('game-over'); | |
| } | |
| } | |
| function stopGame() { | |
| if (gameLoop) { gameLoop.stop(); gameLoop = null; } | |
| } | |
| function startBR() { | |
| if (!selectedDiff) return; | |
| if (!selectedGun) selectedGun = '117'; | |
| startGame(true); | |
| } | |
| // Resize canvas on window resize | |
| window.addEventListener('resize', () => { | |
| const gc = document.getElementById('gameCanvas'); | |
| if (gc) { gc.width = window.innerWidth; gc.height = window.innerHeight; } | |
| }); | |
| // Show main menu on load | |
| showScreen('main-menu'); | |
| </script> | |
| </body> | |
| </html> | |