Upload main.py
Browse files
main.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
Env (HF Space secrets): API_ID, API_HASH, BOT_TOKEN. Optional: PUBLIC_URL, ALLOW_GUEST=1 (browser testing)."""
|
| 3 |
import asyncio, os, json, hmac, hashlib, random, time, math
|
| 4 |
from urllib.parse import parse_qsl
|
|
@@ -67,15 +67,14 @@ def check(init):
|
|
| 67 |
|
| 68 |
# ---------- helpers ----------
|
| 69 |
async def tx(p, o):
|
| 70 |
-
if p.get("bot"): return
|
| 71 |
try: await p["ws"].send_str(j(o))
|
| 72 |
except Exception: pass
|
| 73 |
async def bc(R, o):
|
| 74 |
await asyncio.gather(*[tx(p, o) for p in list(R["players"].values())])
|
| 75 |
async def rs(R):
|
| 76 |
-
await bc(R, {"t": "room", "code": R["code"], "host": R["host"], "mode": R["mode"], "map": R["map"], "state": R["state"],
|
| 77 |
"link": f"https://t.me/{BOT_USER}?start={R['code']}",
|
| 78 |
-
"players": [{"id": p["id"], "name": p["name"], "photo": p["photo"], "team": p["team"]
|
| 79 |
def dirv(P):
|
| 80 |
c = math.cos(P["pitch"]); return (-math.sin(P["yaw"]) * c, math.sin(P["pitch"]), -math.cos(P["yaw"]) * c)
|
| 81 |
def ray_box(o, d, b):
|
|
@@ -97,139 +96,38 @@ def ray_sph(o, d, c, r):
|
|
| 97 |
return t if t >= 0 else (0 if cc < 0 else None)
|
| 98 |
|
| 99 |
async def spawn(P):
|
| 100 |
-
R = P["room"]
|
| 101 |
-
if P.get("bot") and R["train"]:
|
| 102 |
-
x, z = free_point(R); P.update(x=x, y=0, z=z, alive=True); return
|
| 103 |
z = random.uniform(24, 33) * (-1 if P["team"] == "H" else 1)
|
| 104 |
P.update(x=random.uniform(-26, 26), y=0, z=z, alive=True)
|
| 105 |
await tx(P, {"t": "spawn", "x": P["x"], "y": 0, "z": z, "yaw": math.pi if P["team"] == "H" else 0})
|
| 106 |
-
def start_msg(R, P, cd): return {"t": "start", "map": R["map"], "boxes": R["boxes"], "team": P["team"], "cd": cd, "
|
| 107 |
|
| 108 |
async def finish(R, win):
|
| 109 |
if R["state"] != "play": return
|
| 110 |
R["state"] = "end"; await bc(R, {"t": "end", "win": win})
|
| 111 |
async def back():
|
| 112 |
await asyncio.sleep(8)
|
| 113 |
-
if R["code"] in rooms
|
|
|
|
|
|
|
|
|
|
| 114 |
asyncio.create_task(back())
|
| 115 |
|
| 116 |
async def kill(R, K, V, w):
|
| 117 |
-
V["alive"] = False; V["rt"] = time.time() +
|
| 118 |
if K: R["sc"][K["team"]] += 1
|
| 119 |
-
await bc(R, {"t": "kill", "k": K["name"] if K else "", "kt": K["team"] if K else "", "v": V["name"], "vt": V["team"], "w": w})
|
| 120 |
await tx(V, {"t": "dead", "by": K["name"] if K else "", "sec": 3})
|
| 121 |
-
if K and
|
| 122 |
|
| 123 |
async def leave(P):
|
| 124 |
R = P["room"]
|
| 125 |
if R["players"].get(P["id"]) is not P: return
|
| 126 |
del R["players"][P["id"]]
|
| 127 |
-
|
| 128 |
-
if
|
| 129 |
-
if R["host"] == P["id"]: R["host"] = hum[0]["id"]
|
| 130 |
if R["state"] == "play" and len({p["team"] for p in R["players"].values()}) < 2: await finish(R, "T")
|
| 131 |
else: await rs(R)
|
| 132 |
|
| 133 |
-
DIFF = {"easy": (1.6, .35, .5), "normal": (1.0, .55, .7), "hard": (.55, .8, .9)} # aim delay, sniper hit chance, knife hit chance
|
| 134 |
-
NAMES = ["Falcon", "Viper", "Raven", "Ghost", "Blaze", "Nomad", "Storm", "Echo", "Dagger", "Hawk", "Cobra", "Shade"]
|
| 135 |
-
|
| 136 |
-
def free_point(R):
|
| 137 |
-
for _ in range(40):
|
| 138 |
-
x, z = random.uniform(-24, 24), random.uniform(-8, 22)
|
| 139 |
-
if not any(abs(x - b[0]) < b[3] / 2 + 1.2 and abs(z - b[2]) < b[5] / 2 + 1.2 for b in R["boxes"]): return x, z
|
| 140 |
-
return 0.0, 12.0
|
| 141 |
-
|
| 142 |
-
def add_bots(R, kind):
|
| 143 |
-
names = NAMES[:]; random.shuffle(names); n = [0]
|
| 144 |
-
def mk(team, role):
|
| 145 |
-
n[0] += 1
|
| 146 |
-
R["players"][f"b{n[0]}"] = dict(id=f"b{n[0]}", name=names[n[0] % len(names)], photo="", team=team, ws=None, room=R, x=0, y=0, z=0, yaw=0, pitch=0,
|
| 147 |
-
alive=False, rt=0, cd=0, bot=True, role=role, aim=0, think=0, vis=None, d=None, side=0, sa=0, vx=0, stuck=(0, 0, 0))
|
| 148 |
-
if kind == "train":
|
| 149 |
-
for _ in range(3): mk("V", "dummy")
|
| 150 |
-
return
|
| 151 |
-
cnt = {t: sum(1 for p in R["players"].values() if p["team"] == t) for t in "HV"}; per = min(6, max(3, cnt["H"], cnt["V"]))
|
| 152 |
-
for t in "HV":
|
| 153 |
-
for k in range(per - cnt[t]): mk(t, "sniper" if k % 2 == 0 else "knife")
|
| 154 |
-
|
| 155 |
-
async def begin(R, kind, now):
|
| 156 |
-
ps = [p for p in R["players"].values() if not p.get("bot")]
|
| 157 |
-
if kind == "train":
|
| 158 |
-
if len(ps) != 1: return "التدريب فردي، لازم تكون وحدك بالغرفة"
|
| 159 |
-
ps[0]["team"] = "H"
|
| 160 |
-
else:
|
| 161 |
-
if R["mode"] == "random":
|
| 162 |
-
random.shuffle(ps)
|
| 163 |
-
for i, p in enumerate(ps): p["team"] = "HV"[i % 2]
|
| 164 |
-
if kind == "npc":
|
| 165 |
-
for p in ps:
|
| 166 |
-
if p["team"] is None: p["team"] = "H" if sum(q["team"] == "H" for q in ps) <= sum(q["team"] == "V" for q in ps) else "V"
|
| 167 |
-
elif len({p["team"] for p in ps}) < 2 or any(p["team"] is None for p in ps):
|
| 168 |
-
return "لازم كل لاعب يختار فريق، ويكون بكل فريق لاعب واحد على الأقل"
|
| 169 |
-
R.update(state="play", train=kind == "train", sc={"H": 0, "V": 0}, boxes=make_map(R["map"]), go=now + COUNTDOWN, end=now + COUNTDOWN + MATCH_SEC)
|
| 170 |
-
if kind != "pvp": add_bots(R, kind)
|
| 171 |
-
await rs(R)
|
| 172 |
-
for p in list(R["players"].values()): await tx(p, start_msg(R, p, COUNTDOWN)); await spawn(p)
|
| 173 |
-
|
| 174 |
-
async def to_lobby(R):
|
| 175 |
-
for k in [k for k, p in R["players"].items() if p.get("bot")]: del R["players"][k]
|
| 176 |
-
R.update(state="lobby", train=False)
|
| 177 |
-
for p in R["players"].values(): p["alive"] = False
|
| 178 |
-
await rs(R)
|
| 179 |
-
|
| 180 |
-
def bmove(R, b, nx, nz):
|
| 181 |
-
for x, y0, z, w, h, d in R["boxes"]:
|
| 182 |
-
if y0 + h > .55 and y0 < 1.7:
|
| 183 |
-
ox, oz = w / 2 + .4 - abs(nx - x), d / 2 + .4 - abs(nz - z)
|
| 184 |
-
if ox > 0 and oz > 0:
|
| 185 |
-
if ox < oz: nx += math.copysign(ox, nx - x or 1)
|
| 186 |
-
else: nz += math.copysign(oz, nz - z or 1)
|
| 187 |
-
b["x"], b["z"] = max(-34, min(34, nx)), max(-34, min(34, nz))
|
| 188 |
-
|
| 189 |
-
def los(R, a, c):
|
| 190 |
-
o = (a["x"], a["y"] + 1.6, a["z"]); d = [c["x"] - o[0], c["y"] + 1.2 - o[1], c["z"] - o[2]]
|
| 191 |
-
L = math.sqrt(sum(v * v for v in d)) or 1; d = [v / L for v in d]
|
| 192 |
-
return L, d, not any((tb := ray_box(o, d, bx)) is not None and tb < L for bx in R["boxes"])
|
| 193 |
-
|
| 194 |
-
async def bots_tick(R, now):
|
| 195 |
-
if now < R["go"]: return
|
| 196 |
-
aim_t, sh, kh = DIFF[R["diff"]]; ps = list(R["players"].values())
|
| 197 |
-
for b in ps:
|
| 198 |
-
if not b.get("bot") or not b["alive"]: continue
|
| 199 |
-
if R["train"]: # practice dummies: stand or strafe, never shoot
|
| 200 |
-
if now > b["think"]: b["think"] = now + random.uniform(1.5, 3); b["vx"] = random.choice((-3.5, 0, 3.5))
|
| 201 |
-
bmove(R, b, b["x"] + b["vx"] * .05, b["z"]); b["yaw"] = 0; continue
|
| 202 |
-
foes = [p for p in ps if p["alive"] and p["team"] != b["team"]]
|
| 203 |
-
if not foes or R["state"] != "play": continue
|
| 204 |
-
if now >= b["think"]:
|
| 205 |
-
b["think"] = now + random.uniform(.25, .4); b["vis"] = None; bl = 1e9
|
| 206 |
-
for p in foes:
|
| 207 |
-
L, d, ok = los(R, b, p)
|
| 208 |
-
if ok and L < bl: bl, b["vis"], b["d"] = L, p["id"], d
|
| 209 |
-
if math.hypot(b["x"] - b["stuck"][0], b["z"] - b["stuck"][1]) < .25 and not b["vis"]: b["side"] = now + 1.0; b["sa"] = random.uniform(0, 6.28)
|
| 210 |
-
b["stuck"] = (b["x"], b["z"], 0)
|
| 211 |
-
t = R["players"].get(b["vis"]) if b["vis"] else None
|
| 212 |
-
if t and not t["alive"]: t = None; b["vis"] = None
|
| 213 |
-
near = min(foes, key=lambda p: (p["x"] - b["x"]) ** 2 + (p["z"] - b["z"]) ** 2); tg = t or near
|
| 214 |
-
dx, dz = tg["x"] - b["x"], tg["z"] - b["z"]; dist = math.hypot(dx, dz) or 1; b["yaw"] = math.atan2(-dx, -dz)
|
| 215 |
-
sp = 6.0 if b["role"] == "knife" else 3.6
|
| 216 |
-
if b["side"] > now: mx, mz = math.cos(b["sa"]), math.sin(b["sa"])
|
| 217 |
-
elif t and (b["role"] == "sniper" or dist < 1.6): mx = mz = 0
|
| 218 |
-
else: mx, mz = dx / dist, dz / dist
|
| 219 |
-
if mx or mz: bmove(R, b, b["x"] + mx * sp * .05, b["z"] + mz * sp * .05)
|
| 220 |
-
if not t: b["aim"] = 0; continue
|
| 221 |
-
if b["role"] == "knife":
|
| 222 |
-
if dist < 2.2 and now >= b["cd"]:
|
| 223 |
-
b["cd"] = now + .9
|
| 224 |
-
if random.random() < kh: await kill(R, b, t, "k")
|
| 225 |
-
else:
|
| 226 |
-
if not b["aim"]: b["aim"] = now + aim_t * random.uniform(.8, 1.3)
|
| 227 |
-
if now >= b["aim"] and now >= b["cd"]:
|
| 228 |
-
b["cd"] = now + 1.6; b["aim"] = 0; hit = random.random() < sh; d = b["d"] or [0, 0, -1]
|
| 229 |
-
if not hit: d = [d[0] + random.uniform(-.05, .05), d[1] + random.uniform(-.03, .03), d[2] + random.uniform(-.05, .05)]
|
| 230 |
-
await bc(R, {"t": "shot", "o": [b["x"], b["y"] + 1.6, b["z"]], "d": d})
|
| 231 |
-
if hit: await kill(R, b, t, "s")
|
| 232 |
-
|
| 233 |
async def on_msg(ws, P, d):
|
| 234 |
t = d.get("t")
|
| 235 |
if t == "join":
|
|
@@ -243,7 +141,7 @@ async def on_msg(ws, P, d):
|
|
| 243 |
if not R: await ws.send_str(j({"t": "err", "m": "الغرفة غير موجودة"})); return None
|
| 244 |
else:
|
| 245 |
code = "".join(random.choice("ABCDEFGHJKMNPQRSTUVWXYZ23456789") for _ in range(5))
|
| 246 |
-
R = rooms[code] = dict(code=code, host=None, mode="manual", map=0,
|
| 247 |
old = R["players"].get(pid)
|
| 248 |
if not old and (R["state"] != "lobby" or len(R["players"]) >= 8):
|
| 249 |
await ws.send_str(j({"t": "err", "m": "الغرفة ممتلئة أو المباراة بدأت"})); return None
|
|
@@ -252,7 +150,7 @@ async def on_msg(ws, P, d):
|
|
| 252 |
except Exception: pass
|
| 253 |
name = (u.get("first_name", "") + " " + u.get("last_name", "")).strip()[:20] or "لاعب"
|
| 254 |
ph = u.get("photo_url", ""); ph = ph if str(ph).startswith("https://") else ""
|
| 255 |
-
P = dict(id=pid, name=name, photo=ph, team=old["team"] if old else None, ws=ws, room=R, x=0, y=0, z=0, yaw=0, pitch=0, alive=False, rt=0, cd=0)
|
| 256 |
R["players"][pid] = P
|
| 257 |
if R["host"] not in R["players"]: R["host"] = pid
|
| 258 |
await tx(P, {"t": "me", "id": pid}); await rs(R)
|
|
@@ -266,14 +164,19 @@ async def on_msg(ws, P, d):
|
|
| 266 |
R["mode"] = d["m"]; await rs(R)
|
| 267 |
elif t == "map" and host and R["state"] == "lobby" and d.get("m") in (0, 1, 2):
|
| 268 |
R["map"] = d["m"]; await rs(R)
|
| 269 |
-
elif t
|
| 270 |
-
|
| 271 |
-
if
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
| 275 |
elif t == "st" and R["state"] == "play" and P["alive"]:
|
| 276 |
-
try:
|
|
|
|
|
|
|
| 277 |
except Exception: pass
|
| 278 |
elif t == "fall" and R["state"] == "play" and P["alive"] and now >= R["go"]:
|
| 279 |
await kill(R, None, P, "f")
|
|
@@ -288,7 +191,7 @@ async def on_msg(ws, P, d):
|
|
| 288 |
dx, dz = p["x"] - P["x"], p["z"] - P["z"]; dist = math.hypot(dx, dz)
|
| 289 |
if dist < best and abs(p["y"] - P["y"]) < 1.5 and (dx * fx + dz * fz) / (dist * fl + 1e-9) > .35: best, victim = dist, p
|
| 290 |
else:
|
| 291 |
-
await bc(R, {"t": "shot", "o": o, "d": dv}); best = 1e9
|
| 292 |
for p in foes:
|
| 293 |
hs = [ray_sph(o, dv, (p["x"], p["y"] + .9, p["z"]), .55), ray_sph(o, dv, (p["x"], p["y"] + 1.62, p["z"]), .28)]
|
| 294 |
hs = [h for h in hs if h is not None]
|
|
@@ -310,13 +213,12 @@ async def game_loop():
|
|
| 310 |
await asyncio.sleep(.05); now = time.time()
|
| 311 |
for R in list(rooms.values()):
|
| 312 |
if R["state"] != "play": continue
|
| 313 |
-
await bots_tick(R, now)
|
| 314 |
ps = list(R["players"].values())
|
| 315 |
-
await bc(R, {"t": "snap", "sc": [R["sc"]["H"], R["sc"]["V"]], "left":
|
| 316 |
-
"p": [[p["id"], round(p["x"], 2), round(p["y"], 2), round(p["z"], 2), round(p["yaw"], 2), int(p["alive"])] for p in ps]})
|
| 317 |
for p in ps:
|
| 318 |
if not p["alive"] and now >= p["rt"] and now >= R["go"]: await spawn(p)
|
| 319 |
-
if now >= R["end"]
|
| 320 |
a, b = R["sc"]["H"], R["sc"]["V"]; await finish(R, "H" if a > b else "V" if b > a else "T")
|
| 321 |
|
| 322 |
# ---------- bot (MTProto) ----------
|
|
@@ -326,7 +228,7 @@ async def on_start(ev):
|
|
| 326 |
code = (ev.pattern_match.group(1) or "").upper()
|
| 327 |
url = PUBLIC_URL + ("/?room=" + code if code else "/")
|
| 328 |
kb = ReplyInlineMarkup([KeyboardButtonRow([WebBtn("🎮 العب الآن", url)])])
|
| 329 |
-
await ev.respond(f"
|
| 330 |
|
| 331 |
async def run_bot():
|
| 332 |
global BOT_USER
|
|
@@ -345,65 +247,181 @@ async def main():
|
|
| 345 |
|
| 346 |
# ---------- client ----------
|
| 347 |
HTML = r'''<!doctype html><html lang="ar" dir="rtl"><head><meta charset="utf-8">
|
| 348 |
-
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"><title>
|
|
|
|
| 349 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 350 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 351 |
<style>
|
| 352 |
-
:root{color-scheme:dark;--
|
| 353 |
-
*{box-sizing:border-box
|
| 354 |
-
|
| 355 |
-
#
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
.
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
#
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
.
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
#
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
</style></head><body>
|
| 381 |
<canvas id="cv"></canvas>
|
| 382 |
-
<div id="
|
| 383 |
-
<div id="lobby"><
|
| 384 |
-
<
|
| 385 |
-
<
|
| 386 |
-
<div id="
|
| 387 |
-
<div
|
| 388 |
-
<div class="
|
| 389 |
-
<
|
| 390 |
-
<div class="
|
| 391 |
-
<
|
| 392 |
-
<
|
| 393 |
-
|
| 394 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
<script>
|
| 396 |
-
const $=s=>document.querySelector(s),esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 397 |
const TG=window.Telegram&&Telegram.WebApp;if(TG){TG.ready();TG.expand();try{TG.disableVerticalSwipes()}catch(e){}}
|
|
|
|
| 398 |
const qs=new URLSearchParams(location.search),ROOM=(qs.get('room')||(TG&&TG.initDataUnsafe&&TG.initDataUnsafe.start_param)||'').toUpperCase();
|
| 399 |
const touch='ontouchstart' in window;document.body.classList.toggle('touch',touch);
|
| 400 |
-
|
|
|
|
| 401 |
const P={x:0,y:0,z:0,yaw:0,pitch:0,vy:0,ground:true};
|
| 402 |
const send=o=>ws&&ws.readyState===1&&ws.send(JSON.stringify(o)),show=(e,v)=>$(e).style.display=v?(v===1?'block':v):'none';
|
| 403 |
-
function note(t){$('#msg').textContent=t;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
|
| 405 |
ws.onopen=()=>send({t:'join',init:TG?TG.initData:'',room:ROOM,guest:qs.get('guest')||''});
|
| 406 |
-
ws.onclose=()=>note('انقطع الاتصال، أعد فتح اللعبة');
|
| 407 |
ws.onmessage=e=>on(JSON.parse(e.data));
|
| 408 |
function on(m){switch(m.t){
|
| 409 |
case'err':note(m.m);break;
|
|
@@ -411,78 +429,224 @@ function on(m){switch(m.t){
|
|
| 411 |
case'room':Rm=m;if(m.state==='lobby'){endGame();lobby()}break;
|
| 412 |
case'start':startGame(m);break;
|
| 413 |
case'spawn':P.x=m.x;P.y=m.y;P.z=m.z;P.yaw=m.yaw;P.pitch=0;P.vy=0;alive=true;show('#death',0);break;
|
| 414 |
-
case'dead':alive=false;scoped=false;setWp(wp);$('#death').innerHTML='تم ق
|
| 415 |
case'snap':snap(m);break;
|
| 416 |
-
case'kill':{const
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
|
|
|
|
|
|
|
|
|
| 420 |
}}
|
| 421 |
-
function
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
show('#
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
else if(a==='copy'&&Rm){try{navigator.clipboard.writeText(Rm.link);note('تم نسخ الرابط')}catch(_){note(Rm.link)}}});
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
const
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
function look(dx,dy){const s=scoped?.0013:.0036;P.yaw-=dx*s;P.pitch=Math.max(-1.45,Math.min(1.45,P.pitch-dy*s))}
|
| 455 |
-
const
|
| 456 |
-
function move(dt){let fx=0,fz=0;if(keys.KeyW)fz--;if(keys.KeyS)fz++;if(keys.KeyA)fx--;if(keys.KeyD)fx++;fx+=joy.x;fz+=joy.y;const l=Math.hypot(fx,fz);if(l>1){fx/=l;fz/=l}
|
| 457 |
const sp=wp==='k'?7:scoped?2.2:4.8,s=Math.sin(P.yaw),c=Math.cos(P.yaw);let nx=P.x+(fx*c+fz*s)*sp*dt,nz=P.z+(-fx*s+fz*c)*sp*dt;
|
| 458 |
-
for(const[x,y0,z,w,h,d]of boxes){if(P.y<y0+h-.55&&P.y+1.7>y0){const ox=w/2+
|
| 459 |
P.x=Math.max(-34,Math.min(34,nx));P.z=Math.max(-34,Math.min(34,nz));let gr=0;
|
| 460 |
-
for(const[x,y0,z,w,h,d]of boxes){const top=y0+h;if(Math.abs(P.x-x)<w/2+
|
| 461 |
-
if(
|
| 462 |
-
let last=performance.now(),lastSend=0;
|
| 463 |
-
function frame(n){requestAnimationFrame(frame);const dt=Math.min((n-last)/1000,.05);last=n;
|
| 464 |
-
if(playing){
|
| 465 |
-
if(
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
requestAnimationFrame(frame);
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
try{screen.orientation&&screen.orientation.lock&&screen.orientation.lock('landscape').catch(()=>{})}catch(e){}
|
| 474 |
-
// ---- input ----
|
| 475 |
-
const cv=$('#cv');cv.addEventListener('click',()=>{if(!touch&&playing&&alive)cv.requestPointerLock&&cv.requestPointerLock()});
|
| 476 |
document.addEventListener('mousemove',e=>{if(document.pointerLockElement===cv&&playing&&alive)look(e.movementX,e.movementY)});
|
| 477 |
cv.addEventListener('mousedown',e=>{if(document.pointerLockElement!==cv)return;if(e.button===0){fireHeld=true;doFire()}if(e.button===2)toggleScope()});
|
| 478 |
addEventListener('mouseup',e=>{if(e.button===0)fireHeld=false});cv.addEventListener('contextmenu',e=>e.preventDefault());
|
| 479 |
-
addEventListener('keydown',e=>{keys[e.code]=true;if(e.code==='Digit1')setWp('k');if(e.code==='Digit2')setWp('s');if(e.code==='KeyQ')setWp(wp==='k'?'s':'k')});addEventListener('keyup',e=>keys[e.code]=false);
|
| 480 |
let joyId=null,joyO=null,lookId=null,lx=0,ly=0;const jy=$('#joy');
|
| 481 |
cv.addEventListener('touchstart',e=>{e.preventDefault();for(const t of e.changedTouches){if(t.clientX<innerWidth*.45&&joyId===null){joyId=t.identifier;joyO=[t.clientX,t.clientY];jy.style.left=t.clientX+'px';jy.style.top=t.clientY+'px';jy.style.display='block';jy.firstChild.style.transform='none'}else if(lookId===null){lookId=t.identifier;lx=t.clientX;ly=t.clientY}}},{passive:false});
|
| 482 |
cv.addEventListener('touchmove',e=>{e.preventDefault();for(const t of e.changedTouches){if(t.identifier===joyId){let dx=(t.clientX-joyO[0])/50,dy=(t.clientY-joyO[1])/50;const l=Math.hypot(dx,dy);if(l>1){dx/=l;dy/=l}joy.x=dx;joy.y=dy;jy.firstChild.style.transform=`translate(${dx*40}px,${dy*40}px)`}else if(t.identifier===lookId){look((t.clientX-lx)*1.5,(t.clientY-ly)*1.5);lx=t.clientX;ly=t.clientY}}},{passive:false});
|
| 483 |
const tend=e=>{for(const t of e.changedTouches){if(t.identifier===joyId){joyId=null;joy.x=joy.y=0;jy.style.display='none'}if(t.identifier===lookId)lookId=null}};cv.addEventListener('touchend',tend);cv.addEventListener('touchcancel',tend);
|
| 484 |
const bind=(id,dn,up)=>{const b=$(id);b.addEventListener('pointerdown',e=>{e.preventDefault();dn()});['pointerup','pointerleave','pointercancel'].forEach(k=>b.addEventListener(k,()=>up&&up()))};
|
| 485 |
-
bind('#bf',()=>{fireHeld=true;doFire()},()=>fireHeld=false);bind('#bj',()=>keys.Space=true,()=>keys.Space=false);bind('#
|
|
|
|
|
|
|
|
|
|
| 486 |
</script></body></html>'''
|
| 487 |
|
| 488 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
"""CALL OF DUSY: Telegram Mini App game (knife & sniper arena). Bot via MTProto (Telethon), no aiogram.
|
| 2 |
Env (HF Space secrets): API_ID, API_HASH, BOT_TOKEN. Optional: PUBLIC_URL, ALLOW_GUEST=1 (browser testing)."""
|
| 3 |
import asyncio, os, json, hmac, hashlib, random, time, math
|
| 4 |
from urllib.parse import parse_qsl
|
|
|
|
| 67 |
|
| 68 |
# ---------- helpers ----------
|
| 69 |
async def tx(p, o):
|
|
|
|
| 70 |
try: await p["ws"].send_str(j(o))
|
| 71 |
except Exception: pass
|
| 72 |
async def bc(R, o):
|
| 73 |
await asyncio.gather(*[tx(p, o) for p in list(R["players"].values())])
|
| 74 |
async def rs(R):
|
| 75 |
+
await bc(R, {"t": "room", "code": R["code"], "host": R["host"], "mode": R["mode"], "map": R["map"], "state": R["state"],
|
| 76 |
"link": f"https://t.me/{BOT_USER}?start={R['code']}",
|
| 77 |
+
"players": [{"id": p["id"], "name": p["name"], "photo": p["photo"], "team": p["team"]} for p in R["players"].values()]})
|
| 78 |
def dirv(P):
|
| 79 |
c = math.cos(P["pitch"]); return (-math.sin(P["yaw"]) * c, math.sin(P["pitch"]), -math.cos(P["yaw"]) * c)
|
| 80 |
def ray_box(o, d, b):
|
|
|
|
| 96 |
return t if t >= 0 else (0 if cc < 0 else None)
|
| 97 |
|
| 98 |
async def spawn(P):
|
|
|
|
|
|
|
|
|
|
| 99 |
z = random.uniform(24, 33) * (-1 if P["team"] == "H" else 1)
|
| 100 |
P.update(x=random.uniform(-26, 26), y=0, z=z, alive=True)
|
| 101 |
await tx(P, {"t": "spawn", "x": P["x"], "y": 0, "z": z, "yaw": math.pi if P["team"] == "H" else 0})
|
| 102 |
+
def start_msg(R, P, cd): return {"t": "start", "map": R["map"], "boxes": R["boxes"], "team": P["team"], "cd": cd, "win": WIN_KILLS}
|
| 103 |
|
| 104 |
async def finish(R, win):
|
| 105 |
if R["state"] != "play": return
|
| 106 |
R["state"] = "end"; await bc(R, {"t": "end", "win": win})
|
| 107 |
async def back():
|
| 108 |
await asyncio.sleep(8)
|
| 109 |
+
if R["code"] in rooms:
|
| 110 |
+
R["state"] = "lobby"
|
| 111 |
+
for p in R["players"].values(): p["alive"] = False
|
| 112 |
+
await rs(R)
|
| 113 |
asyncio.create_task(back())
|
| 114 |
|
| 115 |
async def kill(R, K, V, w):
|
| 116 |
+
V["alive"] = False; V["rt"] = time.time() + 3
|
| 117 |
if K: R["sc"][K["team"]] += 1
|
| 118 |
+
await bc(R, {"t": "kill", "k": K["name"] if K else "", "kt": K["team"] if K else "", "kid": K["id"] if K else "", "v": V["name"], "vt": V["team"], "vid": V["id"], "w": w})
|
| 119 |
await tx(V, {"t": "dead", "by": K["name"] if K else "", "sec": 3})
|
| 120 |
+
if K and R["sc"][K["team"]] >= WIN_KILLS: await finish(R, K["team"])
|
| 121 |
|
| 122 |
async def leave(P):
|
| 123 |
R = P["room"]
|
| 124 |
if R["players"].get(P["id"]) is not P: return
|
| 125 |
del R["players"][P["id"]]
|
| 126 |
+
if not R["players"]: rooms.pop(R["code"], None); return
|
| 127 |
+
if R["host"] == P["id"]: R["host"] = next(iter(R["players"]))
|
|
|
|
| 128 |
if R["state"] == "play" and len({p["team"] for p in R["players"].values()}) < 2: await finish(R, "T")
|
| 129 |
else: await rs(R)
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
async def on_msg(ws, P, d):
|
| 132 |
t = d.get("t")
|
| 133 |
if t == "join":
|
|
|
|
| 141 |
if not R: await ws.send_str(j({"t": "err", "m": "الغرفة غير موجودة"})); return None
|
| 142 |
else:
|
| 143 |
code = "".join(random.choice("ABCDEFGHJKMNPQRSTUVWXYZ23456789") for _ in range(5))
|
| 144 |
+
R = rooms[code] = dict(code=code, host=None, mode="manual", map=0, state="lobby", players={}, sc={"H": 0, "V": 0}, boxes=[], go=0, end=0)
|
| 145 |
old = R["players"].get(pid)
|
| 146 |
if not old and (R["state"] != "lobby" or len(R["players"]) >= 8):
|
| 147 |
await ws.send_str(j({"t": "err", "m": "الغرفة ممتلئة أو المباراة بدأت"})); return None
|
|
|
|
| 150 |
except Exception: pass
|
| 151 |
name = (u.get("first_name", "") + " " + u.get("last_name", "")).strip()[:20] or "لاعب"
|
| 152 |
ph = u.get("photo_url", ""); ph = ph if str(ph).startswith("https://") else ""
|
| 153 |
+
P = dict(id=pid, name=name, photo=ph, team=old["team"] if old else None, ws=ws, room=R, x=0, y=0, z=0, yaw=0, pitch=0, alive=False, rt=0, cd=0, wp="k")
|
| 154 |
R["players"][pid] = P
|
| 155 |
if R["host"] not in R["players"]: R["host"] = pid
|
| 156 |
await tx(P, {"t": "me", "id": pid}); await rs(R)
|
|
|
|
| 164 |
R["mode"] = d["m"]; await rs(R)
|
| 165 |
elif t == "map" and host and R["state"] == "lobby" and d.get("m") in (0, 1, 2):
|
| 166 |
R["map"] = d["m"]; await rs(R)
|
| 167 |
+
elif t == "start" and host and R["state"] == "lobby":
|
| 168 |
+
ps = list(R["players"].values())
|
| 169 |
+
if R["mode"] == "random":
|
| 170 |
+
random.shuffle(ps)
|
| 171 |
+
for i, p in enumerate(ps): p["team"] = "HV"[i % 2]
|
| 172 |
+
if len({p["team"] for p in ps}) < 2 or any(p["team"] is None for p in ps):
|
| 173 |
+
await tx(P, {"t": "err", "m": "لازم كل لاعب يختار فريق، ويكون بكل فريق لاعب واحد على الأقل"}); return P
|
| 174 |
+
R.update(state="play", sc={"H": 0, "V": 0}, boxes=make_map(R["map"]), go=now + COUNTDOWN, end=now + COUNTDOWN + MATCH_SEC)
|
| 175 |
+
for p in ps: await tx(p, start_msg(R, p, COUNTDOWN)); await spawn(p)
|
| 176 |
elif t == "st" and R["state"] == "play" and P["alive"]:
|
| 177 |
+
try:
|
| 178 |
+
P.update(x=float(d["x"]), y=float(d["y"]), z=float(d["z"]), yaw=float(d["yaw"]), pitch=float(d["pitch"]))
|
| 179 |
+
if d.get("wp") in ("k", "s"): P["wp"] = d["wp"]
|
| 180 |
except Exception: pass
|
| 181 |
elif t == "fall" and R["state"] == "play" and P["alive"] and now >= R["go"]:
|
| 182 |
await kill(R, None, P, "f")
|
|
|
|
| 191 |
dx, dz = p["x"] - P["x"], p["z"] - P["z"]; dist = math.hypot(dx, dz)
|
| 192 |
if dist < best and abs(p["y"] - P["y"]) < 1.5 and (dx * fx + dz * fz) / (dist * fl + 1e-9) > .35: best, victim = dist, p
|
| 193 |
else:
|
| 194 |
+
await bc(R, {"t": "shot", "o": o, "d": dv, "id": P["id"]}); best = 1e9
|
| 195 |
for p in foes:
|
| 196 |
hs = [ray_sph(o, dv, (p["x"], p["y"] + .9, p["z"]), .55), ray_sph(o, dv, (p["x"], p["y"] + 1.62, p["z"]), .28)]
|
| 197 |
hs = [h for h in hs if h is not None]
|
|
|
|
| 213 |
await asyncio.sleep(.05); now = time.time()
|
| 214 |
for R in list(rooms.values()):
|
| 215 |
if R["state"] != "play": continue
|
|
|
|
| 216 |
ps = list(R["players"].values())
|
| 217 |
+
await bc(R, {"t": "snap", "sc": [R["sc"]["H"], R["sc"]["V"]], "left": max(0, int(R["end"] - now)),
|
| 218 |
+
"p": [[p["id"], round(p["x"], 2), round(p["y"], 2), round(p["z"], 2), round(p["yaw"], 2), int(p["alive"]), round(p["pitch"], 2), p["wp"]] for p in ps]})
|
| 219 |
for p in ps:
|
| 220 |
if not p["alive"] and now >= p["rt"] and now >= R["go"]: await spawn(p)
|
| 221 |
+
if now >= R["end"]:
|
| 222 |
a, b = R["sc"]["H"], R["sc"]["V"]; await finish(R, "H" if a > b else "V" if b > a else "T")
|
| 223 |
|
| 224 |
# ---------- bot (MTProto) ----------
|
|
|
|
| 228 |
code = (ev.pattern_match.group(1) or "").upper()
|
| 229 |
url = PUBLIC_URL + ("/?room=" + code if code else "/")
|
| 230 |
kb = ReplyInlineMarkup([KeyboardButtonRow([WebBtn("🎮 العب الآن", url)])])
|
| 231 |
+
await ev.respond(f"🎯 CALL OF DUSY\nتمت دعوتك للغرفة {code} — اضغط الزر وانضم للمعركة!" if code else "🔪🎯 CALL OF DUSY\nمعركة سكاكين وسنايبرات.\nاضغط الزر لإنشاء غرفة وادعُ أصدقاءك.", buttons=kb)
|
| 232 |
|
| 233 |
async def run_bot():
|
| 234 |
global BOT_USER
|
|
|
|
| 247 |
|
| 248 |
# ---------- client ----------
|
| 249 |
HTML = r'''<!doctype html><html lang="ar" dir="rtl"><head><meta charset="utf-8">
|
| 250 |
+
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"><title>CALL OF DUSY</title>
|
| 251 |
+
<meta name="theme-color" content="#0b0718">
|
| 252 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 253 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 254 |
<style>
|
| 255 |
+
:root{color-scheme:dark;--bg:#0b0718;--or:#ff8a3d;--or2:#ffc15e;--cy:#3ee6d0;--rd:#ff5a6e;--tx:#fbefe0;--mut:#b9a9c9;--st:rgba(255,255,255,.14);--hf:Impact,"Arial Black",Haettenschweiler,sans-serif}
|
| 256 |
+
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
| 257 |
+
html,body{margin:0;height:100%;background:var(--bg);color:var(--tx);font-family:"Segoe UI",Tahoma,"Noto Sans Arabic",system-ui,sans-serif;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none}
|
| 258 |
+
canvas#cv{position:fixed;inset:0;width:100%;height:100%}
|
| 259 |
+
svg{display:block}button{font:inherit;cursor:pointer}
|
| 260 |
+
/* ===== lobby ===== */
|
| 261 |
+
#lobby{position:fixed;inset:0;overflow-y:auto;overflow-x:hidden;touch-action:pan-y;background:radial-gradient(130% 70% at 50% -5%,#6b2f78 0,#321a5c 35%,#120a2a 70%,#0b0718 100%)}
|
| 262 |
+
#lobby:before{content:"";position:fixed;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,.03) 0 1px,transparent 1px 44px),repeating-linear-gradient(90deg,rgba(255,255,255,.03) 0 1px,transparent 1px 44px);-webkit-mask-image:linear-gradient(#000,transparent 75%);mask-image:linear-gradient(#000,transparent 75%)}
|
| 263 |
+
.sunb{position:fixed;left:50%;top:-130px;width:430px;height:430px;margin-left:-215px;border-radius:50%;background:radial-gradient(circle,#ffc873 0,#ff7a45 30%,rgba(255,80,90,0) 68%);opacity:.5;animation:glow 7s ease-in-out infinite;pointer-events:none}
|
| 264 |
+
@keyframes glow{50%{opacity:.75;transform:scale(1.08)}}
|
| 265 |
+
.sky{position:fixed;left:0;right:0;bottom:0;width:100%;height:150px;pointer-events:none;opacity:.95}
|
| 266 |
+
#wrap{position:relative;z-index:1;max-width:520px;margin:0 auto;padding:calc(env(safe-area-inset-top,0px) + 12px) 14px calc(env(safe-area-inset-bottom,0px) + 150px);display:flex;flex-direction:column;gap:12px}
|
| 267 |
+
.logo{text-align:center;padding:2px 0 0}
|
| 268 |
+
.logo .em{width:46px;height:46px;margin:0 auto 4px;color:var(--or2);filter:drop-shadow(0 0 10px rgba(255,138,61,.9));animation:spin 14s linear infinite}
|
| 269 |
+
@keyframes spin{to{transform:rotate(360deg)}}
|
| 270 |
+
.logo .l1{font:900 16px/1 var(--hf);letter-spacing:12px;color:var(--or2);text-shadow:0 0 14px #ff8a3d;padding-inline-start:12px}
|
| 271 |
+
.logo .l2{font:900 64px/1 var(--hf);letter-spacing:4px;background:linear-gradient(180deg,#fff3c4 0,#ffb454 45%,#ff5a3d 100%);-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 4px 0 #5a1530) drop-shadow(0 0 20px rgba(255,120,60,.55));padding-inline-start:4px}
|
| 272 |
+
.logo .tag{font-size:12.5px;color:var(--mut);margin-top:8px;letter-spacing:.5px}
|
| 273 |
+
.card{background:linear-gradient(180deg,rgba(255,255,255,.1),rgba(255,255,255,.04));border:1px solid var(--st);border-radius:18px;padding:12px 14px;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:0 8px 24px rgba(0,0,0,.28)}
|
| 274 |
+
.lab{font-size:12px;color:var(--or2);font-weight:800;letter-spacing:.5px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center}
|
| 275 |
+
.rw{display:flex;gap:8px;align-items:center}.sp{flex:1}
|
| 276 |
+
#code{font:900 34px/1 var(--hf);letter-spacing:7px;direction:ltr;color:var(--cy);text-shadow:0 0 16px rgba(62,230,208,.55)}
|
| 277 |
+
.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;border:0;border-radius:14px;padding:11px 16px;font-weight:800;color:var(--tx);background:rgba(255,255,255,.12);border:1px solid var(--st);transition:transform .12s}
|
| 278 |
+
.btn:active{transform:scale(.95)}.btn svg{width:18px;height:18px}.btn:disabled{opacity:.4}
|
| 279 |
+
.btn.pri{color:#2a0f00;border:0;background:linear-gradient(180deg,#ffd27a,#ff8a3d 60%,#ff5a3d);box-shadow:0 6px 18px rgba(255,100,50,.45),inset 0 1px 0 rgba(255,255,255,.6)}
|
| 280 |
+
.btn.sm{padding:9px 13px;font-size:13px}.btn.ic{padding:9px;width:40px;height:40px}
|
| 281 |
+
#snd{position:absolute;top:calc(env(safe-area-inset-top,0px) + 10px);left:14px;z-index:3}
|
| 282 |
+
.row{display:flex;align-items:center;gap:10px;padding:7px 8px;background:rgba(0,0,0,.25);border-radius:12px;margin-top:6px}.row b{flex:1;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 283 |
+
.av{width:38px;height:38px;border-radius:50%;background:#3b2d68;display:flex;align-items:center;justify-content:center;overflow:hidden;font-weight:800;border:2px solid #6a5a95;flex:none}.av.H{border-color:var(--cy)}.av.V{border-color:var(--rd)}.av img{width:100%;height:100%;object-fit:cover}
|
| 284 |
+
.chip{font-style:normal;font-size:11.5px;font-weight:800;padding:3px 10px;border-radius:20px;background:#3b2d68}.chip.H{background:var(--cy);color:#06241f}.chip.V{background:var(--rd);color:#2a0710}
|
| 285 |
+
.tg2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
| 286 |
+
.tcard{--c:#fff;border-radius:16px;padding:10px 8px;text-align:center;border:2px solid transparent;background:rgba(0,0,0,.3);color:var(--c);display:flex;flex-direction:column;align-items:center;gap:2px;transition:transform .12s}
|
| 287 |
+
.tcard.H{--c:#3ee6d0}.tcard.V{--c:#ff5a6e}.tcard svg{width:46px;height:46px}.tcard b{color:#fff;font-size:16px}.tcard small{color:var(--mut);font-size:11.5px}
|
| 288 |
+
.tcard.on{border-color:var(--c);background:linear-gradient(180deg,rgba(255,255,255,.12),rgba(0,0,0,.3));box-shadow:0 0 18px -3px var(--c);transform:translateY(-2px)}
|
| 289 |
+
.seg{display:flex;background:rgba(0,0,0,.35);border-radius:13px;padding:3px;gap:3px}.seg button{flex:1;border:0;background:transparent;color:var(--mut);padding:9px;border-radius:10px;font-weight:800}
|
| 290 |
+
.seg button.on{background:linear-gradient(180deg,#ffd27a,#ff8a3d);color:#2a0f00}
|
| 291 |
+
.maps{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
| 292 |
+
.mp{height:92px;border-radius:14px;border:2px solid transparent;position:relative;overflow:hidden;color:#fff;padding:0;background:var(--g);display:flex;flex-direction:column;justify-content:flex-end;align-items:center;text-shadow:0 1px 6px #000;transition:transform .12s}
|
| 293 |
+
.mp .ic2{position:absolute;top:8px;left:0;right:0;font-size:26px;text-align:center}.mp span{font-size:11.5px;font-weight:800;padding:14px 4px 6px;width:100%;text-align:center;background:linear-gradient(transparent,rgba(0,0,0,.65))}
|
| 294 |
+
.mp.on{border-color:var(--or2);box-shadow:0 0 18px rgba(255,170,80,.6);transform:translateY(-2px)}
|
| 295 |
+
#start{width:100%;font-size:20px;padding:15px;margin-top:12px;animation:pls 2s infinite}
|
| 296 |
+
@keyframes pls{50%{box-shadow:0 6px 30px rgba(255,100,50,.85),inset 0 1px 0 rgba(255,255,255,.6)}}
|
| 297 |
+
#wait{text-align:center;color:var(--mut);padding:10px}#wait i{font-style:normal;animation:bl 1.2s infinite}@keyframes bl{50%{opacity:.2}}
|
| 298 |
+
.ctl{font-size:11.5px;color:var(--mut);text-align:center;line-height:1.7}
|
| 299 |
+
#msg{display:none;position:fixed;top:calc(env(safe-area-inset-top,0px) + 10px);left:50%;transform:translateX(-50%);z-index:20;max-width:90%;padding:10px 16px;border-radius:14px;background:rgba(120,25,60,.95);border:1px solid #ff7a9a;font-weight:700;text-align:center;box-shadow:0 8px 24px rgba(0,0,0,.5)}
|
| 300 |
+
/* ===== HUD ===== */
|
| 301 |
+
#hud{position:fixed;inset:0;display:none;pointer-events:none}
|
| 302 |
+
#hud *{pointer-events:none}#hud .b,#hud .tg,.touch #hud .slot{pointer-events:auto}
|
| 303 |
+
#sc{position:absolute;direction:ltr;top:calc(env(safe-area-inset-top,0px) + 8px);left:50%;transform:translateX(-50%);display:flex;align-items:stretch;border-radius:14px;overflow:hidden;box-shadow:0 6px 20px rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.18);background:rgba(10,6,24,.6);backdrop-filter:blur(6px)}
|
| 304 |
+
#sc .tm{min-width:84px;padding:5px 10px 9px;text-align:center;position:relative}
|
| 305 |
+
#sc .tm b{display:block;font:900 26px/1 var(--hf)}#sc .tm small{font-size:10.5px;opacity:.9}
|
| 306 |
+
#sc .H{background:linear-gradient(180deg,rgba(62,230,208,.38),rgba(62,230,208,.06));color:#c6fff6}#sc .V{background:linear-gradient(180deg,rgba(255,90,110,.38),rgba(255,90,110,.06));color:#ffd5db}
|
| 307 |
+
#sc .clk{padding:5px 12px;font:900 21px/1 var(--hf);display:flex;align-items:center;letter-spacing:1px;min-width:68px;justify-content:center}#sc .clk.lo{color:#ff6a6a;animation:bl 1s infinite}
|
| 308 |
+
#sc .bar{position:absolute;left:0;right:0;bottom:0;height:4px;background:rgba(0,0,0,.45)}#sc .bar i{display:block;height:100%;width:0;background:currentColor;transition:width .3s}
|
| 309 |
+
#radar{position:absolute;left:10px;top:calc(env(safe-area-inset-top,0px) + 8px);width:98px;height:98px;border-radius:50%;border:2px solid rgba(255,190,120,.7);box-shadow:0 0 14px rgba(0,0,0,.5)}
|
| 310 |
+
#kd{position:absolute;left:10px;top:calc(env(safe-area-inset-top,0px) + 112px);font-size:12px;font-weight:800;background:rgba(10,6,24,.6);border:1px solid var(--st);padding:3px 10px;border-radius:10px;direction:rtl}
|
| 311 |
+
#feed{position:absolute;top:calc(env(safe-area-inset-top,0px) + 74px);right:10px;display:flex;flex-direction:column;gap:4px;align-items:flex-end}
|
| 312 |
+
.kf{direction:ltr;display:flex;align-items:center;gap:6px;font-size:12.5px;padding:3px 10px;border-radius:10px;background:rgba(10,6,24,.62);border:1px solid transparent;animation:fin .25s}.kf.me{border-color:var(--or2);background:rgba(90,40,10,.7)}
|
| 313 |
+
.kf i{width:20px;height:20px;color:#ffe9b0;display:block}.kf i svg{width:100%;height:100%}@keyframes fin{from{opacity:0;transform:translateX(20px)}}
|
| 314 |
+
#xh{position:absolute;left:50%;top:50%;width:0;height:0}#xh i{position:absolute;background:#fff;box-shadow:0 0 2px #000,0 0 4px #000}
|
| 315 |
+
#xh i:nth-child(1){width:2px;height:9px;left:-1px;top:-18px}#xh i:nth-child(2){width:2px;height:9px;left:-1px;top:9px}#xh i:nth-child(3){width:9px;height:2px;left:-18px;top:-1px}#xh i:nth-child(4){width:9px;height:2px;left:9px;top:-1px}
|
| 316 |
+
#xh u{position:absolute;left:-1.5px;top:-1.5px;width:3px;height:3px;border-radius:50%;background:#ff5a3d}
|
| 317 |
+
#hm{position:absolute;left:50%;top:50%;width:0;height:0;opacity:0}#hm i{position:absolute;width:3px;height:11px;background:#fff;box-shadow:0 0 4px #000}
|
| 318 |
+
#hm i:nth-child(1){transform:translate(-18px,-18px) rotate(45deg)}#hm i:nth-child(2){transform:translate(16px,-18px) rotate(-45deg)}#hm i:nth-child(3){transform:translate(-18px,8px) rotate(-45deg)}#hm i:nth-child(4){transform:translate(16px,8px) rotate(45deg)}
|
| 319 |
+
#hm.f{animation:hmf .35s}@keyframes hmf{0%{opacity:1;transform:scale(1.5)}100%{opacity:0;transform:scale(1)}}#hm.f i{background:#ff5a3d}
|
| 320 |
+
#ban{position:absolute;left:0;right:0;top:24%;text-align:center;opacity:0}#ban small{display:block;font-size:12px;color:var(--or2);letter-spacing:2px}#ban b{font:900 28px/1.1 var(--hf);text-shadow:0 2px 14px #000,0 0 18px #ff5a3d}
|
| 321 |
+
#ban.f{animation:bnf 1.6s}@keyframes bnf{0%{opacity:0;transform:scale(1.6)}12%{opacity:1;transform:scale(1)}80%{opacity:1}100%{opacity:0;transform:translateY(-20px)}}
|
| 322 |
+
#cdn,#death,#over{position:absolute;inset:0;display:none;align-items:center;justify-content:center;text-align:center;flex-direction:column;font-weight:900;text-shadow:0 2px 12px #000}
|
| 323 |
+
#cdn{font:900 110px/1 var(--hf);color:var(--or2);text-shadow:0 0 30px #ff6a3d,0 4px 0 #5a1530}#cdn small{font:700 15px "Segoe UI",Tahoma,sans-serif;letter-spacing:3px;color:#fff}
|
| 324 |
+
#death{background:radial-gradient(circle,rgba(110,10,30,.15) 30%,rgba(120,10,30,.7) 100%);gap:6px}#death .dt{font:900 30px var(--hf);color:#ff7a8a;letter-spacing:2px}#death .dby{font-size:16px}#death .dsec{font:900 54px var(--hf);color:#fff}
|
| 325 |
+
#over{background:rgba(10,6,24,.82);gap:6px;backdrop-filter:blur(4px)}#over .rs{font:900 64px/1 var(--hf);letter-spacing:3px;animation:pop .5s}#over .rs.w{background:linear-gradient(#fff3c4,#ffb454);-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 0 20px #ff8a3d)}#over .rs.l{color:#ff6a7a}#over .rs.t{color:#cbd}
|
| 326 |
+
#over .big{font:900 34px var(--hf);direction:ltr}#over .sm{font-size:14px;opacity:.85;font-weight:600}@keyframes pop{from{transform:scale(2);opacity:0}}
|
| 327 |
+
#scope{position:absolute;inset:0;display:none;background:radial-gradient(circle at 50% 50%,transparent 0,transparent 26vmin,rgba(0,0,0,.5) 29.6vmin,#000 30.5vmin)}
|
| 328 |
+
#scope svg{position:absolute;left:50%;top:50%;width:60vmin;height:60vmin;margin:-30vmin 0 0 -30vmin}
|
| 329 |
+
#scope b{position:absolute;left:50%;top:calc(50% + 31.5vmin);transform:translateX(-50%);font:700 11px monospace;color:#9fe;opacity:.8;letter-spacing:2px}
|
| 330 |
+
#cdbar{position:absolute;left:50%;transform:translateX(-50%);bottom:calc(env(safe-area-inset-bottom,0px) + 68px);width:130px;height:5px;border-radius:3px;background:rgba(255,255,255,.18);overflow:hidden;opacity:0;transition:opacity .15s}#cdbar i{display:block;height:100%;background:linear-gradient(90deg,#ffb454,#ff6a4d);width:0}
|
| 331 |
+
#slots{position:absolute;left:50%;transform:translateX(-50%);bottom:calc(env(safe-area-inset-bottom,0px) + 14px);display:flex;gap:8px;direction:ltr}
|
| 332 |
+
.slot{position:relative;width:84px;height:48px;border-radius:13px;background:rgba(10,6,24,.6);border:1.5px solid rgba(255,255,255,.22);color:#b9a9c9;display:flex;align-items:center;justify-content:center;padding:7px 12px;transition:all .15s}
|
| 333 |
+
.slot.on{border-color:var(--or2);color:#fff;box-shadow:0 0 14px rgba(255,170,80,.55);background:linear-gradient(180deg,rgba(255,170,80,.38),rgba(10,6,24,.65))}
|
| 334 |
+
.slot span{position:absolute;top:2px;left:7px;font:800 10px sans-serif;opacity:.75}.slot svg{width:100%;height:100%}
|
| 335 |
+
#hint{position:absolute;left:0;right:0;bottom:calc(env(safe-area-inset-bottom,0px) + 82px);text-align:center;font-size:11.5px;color:#fff;text-shadow:0 1px 6px #000;opacity:.85;transition:opacity 1s}
|
| 336 |
+
#hud .tg{position:absolute;right:10px;top:calc(env(safe-area-inset-top,0px) + 8px);width:36px;height:36px;padding:7px;border-radius:50%;background:rgba(10,6,24,.6);border:1px solid var(--st);color:#fff}
|
| 337 |
+
.b{display:none;position:absolute;align-items:center;justify-content:center;border-radius:50%;background:radial-gradient(circle at 35% 30%,rgba(255,255,255,.3),rgba(255,255,255,.07));border:2px solid rgba(255,255,255,.42);color:#fff;touch-action:none;box-shadow:0 4px 14px rgba(0,0,0,.45);padding:0}
|
| 338 |
+
.touch .b{display:flex}.b svg{width:52%;height:52%;filter:drop-shadow(0 1px 2px #000)}.b:active{transform:scale(.92)}
|
| 339 |
+
#bf{right:20px;bottom:calc(env(safe-area-inset-bottom,0px) + 34px);width:94px;height:94px;background:radial-gradient(circle at 35% 30%,rgba(255,190,110,.8),rgba(255,70,60,.5));border-color:#ffb26e;box-shadow:0 0 22px rgba(255,100,50,.55)}
|
| 340 |
+
#bj{right:126px;bottom:calc(env(safe-area-inset-bottom,0px) + 28px);width:62px;height:62px}
|
| 341 |
+
#bs{right:28px;bottom:calc(env(safe-area-inset-bottom,0px) + 140px);width:62px;height:62px;background:radial-gradient(circle at 35% 30%,rgba(120,230,255,.5),rgba(255,255,255,.08));border-color:#8fe6ff}
|
| 342 |
+
#joy{position:absolute;display:none;width:110px;height:110px;margin:-55px;border-radius:50%;border:2px solid rgba(255,255,255,.4);background:radial-gradient(circle,rgba(255,255,255,.12),rgba(255,255,255,.04));box-shadow:0 0 16px rgba(0,0,0,.4)}
|
| 343 |
+
#joy i{position:absolute;left:50%;top:50%;width:48px;height:48px;margin:-24px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#fff,#ffcf8a);opacity:.85}
|
| 344 |
</style></head><body>
|
| 345 |
<canvas id="cv"></canvas>
|
| 346 |
+
<div id="msg"></div>
|
| 347 |
+
<div id="lobby"><div class="sunb"></div>
|
| 348 |
+
<svg class="sky" viewBox="0 0 400 150" preserveAspectRatio="none"><polygon fill="#1a0f3a" points="0,150 0,92 24,92 24,72 44,72 44,96 60,96 60,64 82,64 82,98 100,98 100,84 128,84 128,100 142,100 142,68 166,68 166,98 182,98 182,108 206,108 206,80 226,80 226,104 244,104 244,90 266,90 266,60 284,60 284,96 300,96 300,74 322,74 322,100 340,100 340,86 362,86 362,106 380,106 380,70 400,70 400,150"/><polygon fill="#0d0820" points="0,150 0,112 18,112 18,98 40,98 40,116 70,116 70,92 92,92 92,118 120,118 120,104 150,104 150,120 176,120 176,100 200,100 200,122 232,122 232,96 256,96 256,118 290,118 290,102 312,102 312,120 346,120 346,98 372,98 372,114 400,114 400,150"/></svg>
|
| 349 |
+
<button class="btn ic" id="snd" data-a="snd"></button>
|
| 350 |
+
<div id="wrap">
|
| 351 |
+
<div class="logo"><svg class="em" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="24" cy="24" r="16"/><circle cx="24" cy="24" r="3" fill="currentColor"/><path d="M24 2v10M24 36v10M2 24h10M36 24h10"/></svg><div class="l1">CALL OF</div><div class="l2">DUSY</div><div class="tag">سكاكين • سنايبرات • معركة الغسق</div></div>
|
| 352 |
+
<div class="card"><div class="lab"><span>كود الغرفة</span></div><div class="rw"><div id="code">-----</div><div class="sp"></div><button class="btn pri sm" data-a="share" id="bshare"></button><button class="btn ic" data-a="copy" id="bcopy"></button></div></div>
|
| 353 |
+
<div class="card"><div class="lab"><span>اللاعبون</span><span id="pc">0/8</span></div><div id="plist"></div></div>
|
| 354 |
+
<div class="card" id="teams"><div class="lab"><span>اختر فريقك</span></div><div class="tg2">
|
| 355 |
+
<button class="tcard H" data-a="team:H" id="tH"><svg viewBox="0 0 48 48"><path d="M24 3 6 10v14c0 10 7 17 18 21 11-4 18-11 18-21V10Z" fill="currentColor" fill-opacity=".18" stroke="currentColor" stroke-width="2.5"/><path d="M9 22 24 13l15 9-15-3Zm7 7 8-4 8 4-8 9Z" fill="currentColor"/></svg><b>الصقور</b><small id="cH">0 لاعب</small></button>
|
| 356 |
+
<button class="tcard V" data-a="team:V" id="tV"><svg viewBox="0 0 48 48"><path d="M24 3 6 10v14c0 10 7 17 18 21 11-4 18-11 18-21V10Z" fill="currentColor" fill-opacity=".18" stroke="currentColor" stroke-width="2.5"/><path d="M12 13l6 7h12l6-7v13l-6 10H18l-6-10Z" fill="currentColor"/><circle cx="19" cy="26" r="2" fill="#140c2b"/><circle cx="29" cy="26" r="2" fill="#140c2b"/></svg><b>الذئاب</b><small id="cV">0 لاعب</small></button></div></div>
|
| 357 |
+
<div class="card" id="hostctl" style="display:none"><div class="lab"><span>إعدادات المباراة</span></div>
|
| 358 |
+
<div class="seg" style="margin-bottom:10px"><button data-a="mode:manual" id="mm">اختيار يدوي</button><button data-a="mode:random" id="mr">فرق عشوائية</button></div>
|
| 359 |
+
<div class="maps"><button class="mp" data-a="map:0" id="m0" style="--g:linear-gradient(180deg,#3a2a7a,#d9607a 45%,#ffb066)"><div class="ic2">🏜️</div><span>أطلال الصحراء</span></button><button class="mp" data-a="map:1" id="m1" style="--g:linear-gradient(180deg,#16254f,#6f86b8 50%,#e9dfe9)"><div class="ic2">❄️</div><span>القاعدة الثلجية</span></button><button class="mp" data-a="map:2" id="m2" style="--g:linear-gradient(180deg,#050512,#3a1a6a 50%,#c4506a)"><div class="ic2">🌃</div><span>أسطح المدينة</span></button></div>
|
| 360 |
+
<button class="btn pri" id="start" data-a="start">ابدأ المعركة</button></div>
|
| 361 |
+
<div id="wait" style="display:none">بانتظار صاحب الغرفة ليبدأ المباراة <i>● ● ●</i></div>
|
| 362 |
+
<div class="ctl">الجوال: عصا يسار للحركة • اسحب لليمين للتصويب<br>الكمبيوتر: WASD + ماوس • 1/2 تبديل السلاح • كليك يمين منظار</div>
|
| 363 |
+
</div></div>
|
| 364 |
+
<div id="hud">
|
| 365 |
+
<div id="sc"><div class="tm H"><b id="sH">0</b><small>الصقور</small><div class="bar" style="color:#3ee6d0"><i id="bH"></i></div></div><div class="clk" id="clk">5:00</div><div class="tm V"><b id="sV">0</b><small>الذئاب</small><div class="bar" style="color:#ff5a6e"><i id="bV"></i></div></div></div>
|
| 366 |
+
<canvas id="radar" width="200" height="200"></canvas><div id="kd"></div>
|
| 367 |
+
<div id="feed"></div><button class="tg" id="snd2" data-a="snd"></button>
|
| 368 |
+
<div id="xh"><i></i><i></i><i></i><i></i><u></u></div><div id="hm"><i></i><i></i><i></i><i></i></div>
|
| 369 |
+
<div id="scope"><svg viewBox="-50 -50 100 100" stroke="#000" fill="#000"><g stroke-linecap="butt"><line x1="-50" y1="0" x2="-14" y2="0" stroke-width="1.2"/><line x1="14" y1="0" x2="50" y2="0" stroke-width="1.2"/><line x1="0" y1="-50" x2="0" y2="-14" stroke-width="1.2"/><line x1="0" y1="14" x2="0" y2="50" stroke-width="1.2"/><line x1="-14" y1="0" x2="14" y2="0" stroke-width=".22"/><line x1="0" y1="-14" x2="0" y2="14" stroke-width=".22"/></g><g stroke="none"><circle cx="-5" cy="0" r=".55"/><circle cx="5" cy="0" r=".55"/><circle cx="-9" cy="0" r=".55"/><circle cx="9" cy="0" r=".55"/><circle cx="0" cy="-5" r=".55"/><circle cx="0" cy="5" r=".55"/><circle cx="0" cy="-9" r=".55"/><circle cx="0" cy="9" r=".55"/><circle cx="0" cy="0" r=".4" fill="#f33"/></g></svg><b>x18</b></div>
|
| 370 |
+
<div id="ban"></div><div id="cdn"></div><div id="death"></div><div id="over"></div>
|
| 371 |
+
<div id="hint"></div><div id="cdbar"><i></i></div>
|
| 372 |
+
<div id="slots"><div class="slot" id="s1" data-w="k"><span>1</span></div><div class="slot" id="s2" data-w="s"><span>2</span></div></div>
|
| 373 |
+
<div id="joy"><i></i></div>
|
| 374 |
+
<button class="b" id="bf"></button><button class="b" id="bj"></button><button class="b" id="bs"></button>
|
| 375 |
+
</div>
|
| 376 |
<script>
|
| 377 |
+
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)],esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 378 |
const TG=window.Telegram&&Telegram.WebApp;if(TG){TG.ready();TG.expand();try{TG.disableVerticalSwipes()}catch(e){}}
|
| 379 |
+
const hap=(t)=>{try{TG&&TG.HapticFeedback&&TG.HapticFeedback.impactOccurred(t||'light')}catch(e){}};
|
| 380 |
const qs=new URLSearchParams(location.search),ROOM=(qs.get('room')||(TG&&TG.initDataUnsafe&&TG.initDataUnsafe.start_param)||'').toUpperCase();
|
| 381 |
const touch='ontouchstart' in window;document.body.classList.toggle('touch',touch);
|
| 382 |
+
const TN={H:'الصقور',V:'الذئاب'},TCOL={H:'#3ee6d0',V:'#ff5a6e'};
|
| 383 |
+
let ws,me,Rm,boxes=[],others={},alive=false,playing=false,goAt=0,wp='k',scoped=false,cd=0,cdStart=0,cdLen=0,myTeam='H',fireHeld=false,mapI=0,keys={},joy={x:0,y:0},WIN=25,kills=0,deaths=0,lastSc=[0,0],mv=0,deathEnd=0,lastCd=0,hintT=0;
|
| 384 |
const P={x:0,y:0,z:0,yaw:0,pitch:0,vy:0,ground:true};
|
| 385 |
const send=o=>ws&&ws.readyState===1&&ws.send(JSON.stringify(o)),show=(e,v)=>$(e).style.display=v?(v===1?'block':v):'none';
|
| 386 |
+
let noteT=0;function note(t,sticky){const m=$('#msg');m.textContent=t;m.style.display='block';clearTimeout(noteT);if(!sticky)noteT=setTimeout(()=>m.style.display='none',4200)}
|
| 387 |
+
/* ---------- icons ---------- */
|
| 388 |
+
const IC={
|
| 389 |
+
k:'<svg viewBox="0 0 64 64" fill="currentColor"><polygon points="2.8,54.8 9.2,61.2 25.2,45.2 18.8,38.8"/><polygon points="17,35 29,47 31.5,44.5 19.5,32.5"/><path d="M24 34 54 4Q60 2 60 4Q56 26 31 41Z"/></svg>',
|
| 390 |
+
s:'<svg viewBox="0 0 96 32" fill="currentColor"><polygon points="2,10 24,12 24,19 12,24 2,20"/><rect x="24" y="11" width="26" height="8"/><rect x="50" y="13" width="44" height="3.5"/><rect x="90" y="11.5" width="5" height="6.5"/><rect x="30" y="3" width="26" height="5" rx="2.5"/><rect x="54" y="1.5" width="6" height="8" rx="1"/><rect x="26" y="2.5" width="5" height="6" rx="1"/><rect x="34" y="8" width="3" height="3"/><rect x="48" y="8" width="3" height="3"/><polygon points="26,19 32,19 30,28 24,28"/><rect x="38" y="19" width="7" height="6"/></svg>',
|
| 391 |
+
cross:'<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"><circle cx="24" cy="24" r="13"/><path d="M24 4v10M24 34v10M4 24h10M34 24h10"/></svg>',
|
| 392 |
+
jump:'<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"><path d="M24 40V10M11 22l13-13 13 13"/></svg>',
|
| 393 |
+
scope:'<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"><circle cx="24" cy="24" r="17"/><path d="M24 2v14M24 32v14M2 24h14M32 24h14"/><circle cx="24" cy="24" r="2" fill="currentColor"/></svg>',
|
| 394 |
+
skull:'<svg viewBox="0 0 48 48" fill="currentColor"><path d="M24 4C13 4 7 12 7 21c0 6 3 9 6 11v8h22v-8c3-2 6-5 6-11C41 12 35 4 24 4Zm-8 22a4 4 0 1 1 0-8 4 4 0 0 1 0 8Zm16 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8Z"/></svg>',
|
| 395 |
+
share:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 10.5 6.8-4M8.6 13.5l6.8 4"/></svg>',
|
| 396 |
+
copy:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h8"/></svg>',
|
| 397 |
+
on:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H2v6h4l5 4Z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg>',
|
| 398 |
+
off:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H2v6h4l5 4Z"/><path d="m22 9-6 6M16 9l6 6"/></svg>'};
|
| 399 |
+
$('#bshare').innerHTML=IC.share+'<span>دعوة</span>';$('#bcopy').innerHTML=IC.copy;$('#s1').insertAdjacentHTML('beforeend',IC.k);$('#s2').insertAdjacentHTML('beforeend',IC.s);$('#bj').innerHTML=IC.jump;$('#bs').innerHTML=IC.scope;
|
| 400 |
+
/* ---------- audio (synth) ---------- */
|
| 401 |
+
let AC=null,muted=false,NB=null;
|
| 402 |
+
function ac(){if(!AC){try{AC=new(window.AudioContext||window.webkitAudioContext)()}catch(e){}}if(AC&&AC.state==='suspended')AC.resume();return AC}
|
| 403 |
+
function nbuf(){if(NB)return NB;const a=ac(),n=a.sampleRate;NB=a.createBuffer(1,n,a.sampleRate);const d=NB.getChannelData(0);for(let i=0;i<n;i++)d[i]=Math.random()*2-1;return NB}
|
| 404 |
+
function snd(k,v){if(muted)return;v=v==null?1:v;const a=ac();if(!a)return;const t=a.currentTime;
|
| 405 |
+
const env=(g,pk,dur,at)=>{g.gain.setValueAtTime(.0001,at);g.gain.exponentialRampToValueAtTime(Math.max(.0002,pk),at+.006);g.gain.exponentialRampToValueAtTime(.0001,at+dur)};
|
| 406 |
+
const nz=(dur,type,f0,f1,pk,at,q)=>{at=at||t;const s=a.createBufferSource();s.buffer=nbuf();const f=a.createBiquadFilter();f.type=type;f.Q.value=q||1;f.frequency.setValueAtTime(f0,at);f.frequency.exponentialRampToValueAtTime(f1,at+dur);const g=a.createGain();env(g,pk*v,dur,at);s.connect(f);f.connect(g);g.connect(a.destination);s.start(at,Math.random()*.4,dur+.05)};
|
| 407 |
+
const tn=(type,f0,f1,dur,pk,at)=>{at=at||t;const o=a.createOscillator();o.type=type;o.frequency.setValueAtTime(f0,at);o.frequency.exponentialRampToValueAtTime(f1,at+dur);const g=a.createGain();env(g,pk*v,dur,at);o.connect(g);g.connect(a.destination);o.start(at);o.stop(at+dur+.05)};
|
| 408 |
+
switch(k){
|
| 409 |
+
case'snipe':nz(.55,'lowpass',2800,140,1,t);nz(.07,'highpass',3000,7000,.7,t);tn('sine',150,32,.4,.9);nz(.8,'lowpass',600,90,.28,t+.14);break;
|
| 410 |
+
case'knife':nz(.22,'bandpass',500,3600,.7,t,2);break;
|
| 411 |
+
case'bolt':tn('square',320,180,.05,.12,t+.02);tn('square',180,340,.05,.12,t+.15);break;
|
| 412 |
+
case'swap':nz(.06,'highpass',2000,4500,.25);break;
|
| 413 |
+
case'hit':tn('sine',880,880,.1,.5);tn('sine',1320,1320,.16,.5,t+.09);break;
|
| 414 |
+
case'die':tn('sawtooth',230,45,.65,.3);break;
|
| 415 |
+
case'tick':tn('sine',660,660,.1,.4);break;
|
| 416 |
+
case'go':tn('sine',990,990,.4,.5);break;
|
| 417 |
+
case'win':[523,659,784,1047].forEach((f,i)=>tn('triangle',f,f,.3,.4,t+i*.12));break;
|
| 418 |
+
case'lose':[392,330,262].forEach((f,i)=>tn('triangle',f,f*.97,.4,.35,t+i*.18));break}}
|
| 419 |
+
addEventListener('pointerdown',()=>ac(),{once:true});
|
| 420 |
+
function setSnd(){const h=IC[muted?'off':'on'];$('#snd').innerHTML=h;$('#snd2').innerHTML=h}setSnd();
|
| 421 |
+
/* ---------- network ---------- */
|
| 422 |
ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
|
| 423 |
ws.onopen=()=>send({t:'join',init:TG?TG.initData:'',room:ROOM,guest:qs.get('guest')||''});
|
| 424 |
+
ws.onclose=()=>note('انقطع الاتصال، أعد فتح اللعبة',1);
|
| 425 |
ws.onmessage=e=>on(JSON.parse(e.data));
|
| 426 |
function on(m){switch(m.t){
|
| 427 |
case'err':note(m.m);break;
|
|
|
|
| 429 |
case'room':Rm=m;if(m.state==='lobby'){endGame();lobby()}break;
|
| 430 |
case'start':startGame(m);break;
|
| 431 |
case'spawn':P.x=m.x;P.y=m.y;P.z=m.z;P.yaw=m.yaw;P.pitch=0;P.vy=0;alive=true;show('#death',0);break;
|
| 432 |
+
case'dead':alive=false;scoped=false;setWp(wp);deathEnd=performance.now()+m.sec*1000;$('#death').innerHTML='<div class="dt">تم القضاء عليك</div>'+(m.by?'<div class="dby">بواسطة <b>'+esc(m.by)+'</b></div>':'')+'<div class="dsec" id="dsec">'+m.sec+'</div>';show('#death','flex');break;
|
| 433 |
case'snap':snap(m);break;
|
| 434 |
+
case'kill':{const c=t=>TCOL[t]||'#fff',ic=m.w==='k'?IC.k:m.w==='s'?IC.s:IC.skull,d=document.createElement('div');d.className='kf'+(m.kid===me?' me':'');
|
| 435 |
+
d.innerHTML=(m.k?`<b style="color:${c(m.kt)}">${esc(m.k)}</b>`:'')+`<i>${ic}</i><b style="color:${c(m.vt)}">${esc(m.v)}</b>`;$('#feed').prepend(d);while($('#feed').children.length>5)$('#feed').lastChild.remove();setTimeout(()=>d.remove(),4500);
|
| 436 |
+
if(m.kid===me&&m.k){kills++;banner(m.v);snd('hit');hap('heavy')}
|
| 437 |
+
if(m.vid===me){deaths++;snd('die');hap('rigid')}updKD();break}
|
| 438 |
+
case'shot':{const mine=m.id===me,o=new THREE.Vector3(...m.o),dv=new THREE.Vector3(...m.d);tracer(o,dv,mine);if(!mine)snd('snipe',Math.max(.1,Math.min(1,1/(1+o.distanceTo(cam.position)*.07))));break}
|
| 439 |
+
case'end':{alive=false;scoped=false;document.exitPointerLock&&document.exitPointerLock();const w=m.win,cls=w==='T'?'t':w===myTeam?'w':'l',txt=w==='T'?'تعادل':w===myTeam?'النصر':'الهزيمة';
|
| 440 |
+
$('#over').innerHTML=`<div class="rs ${cls}">${txt}</div><div class="big"><span style="color:#3ee6d0">${lastSc[0]}</span> : <span style="color:#ff5a6e">${lastSc[1]}</span></div><div class="sm">${w==='T'?'':'فاز فريق '+TN[w]}</div><div class="sm">قتلاتك ${kills} • موتاتك ${deaths}</div><div class="sm" style="opacity:.6;margin-top:10px">العودة إلى الغرفة خلال ثوانٍ...</div>`;show('#over','flex');snd(cls==='w'?'win':'lose');hap(cls==='w'?'heavy':'light');break}
|
| 441 |
}}
|
| 442 |
+
function banner(n){const b=$('#ban');b.innerHTML='<small>تم القضاء على</small><b>'+esc(n)+'</b>';b.classList.remove('f');void b.offsetWidth;b.classList.add('f');const h=$('#hm');h.classList.remove('f');void h.offsetWidth;h.classList.add('f')}
|
| 443 |
+
function updKD(){$('#kd').textContent='قتل '+kills+' • موت '+deaths}
|
| 444 |
+
/* ---------- lobby ---------- */
|
| 445 |
+
function lobby(){show('#lobby',1);show('#hud',0);const host=Rm.host===me;$('#code').textContent=Rm.code;$('#pc').textContent=Rm.players.length+'/8';
|
| 446 |
+
$('#plist').innerHTML=Rm.players.map(p=>`<div class="row"><span class="av ${p.team||''}">${p.photo?`<img src="${esc(p.photo)}">`:esc((p.name||'?')[0])}</span><b>${esc(p.name)}${p.id===Rm.host?' 👑':''}${p.id===me?' (أنت)':''}</b><em class="chip ${p.team||''}">${p.team?TN[p.team]:'—'}</em></div>`).join('');
|
| 447 |
+
const mine=(Rm.players.find(p=>p.id===me)||{}).team,cnt=t=>Rm.players.filter(p=>p.team===t).length;
|
| 448 |
+
show('#teams',Rm.mode==='manual'?1:0);$('#tH').classList.toggle('on',mine==='H');$('#tV').classList.toggle('on',mine==='V');$('#cH').textContent=cnt('H')+' لاعب';$('#cV').textContent=cnt('V')+' لاعب';
|
| 449 |
+
show('#hostctl',host?1:0);show('#wait',host?0:1);$('#mm').classList.toggle('on',Rm.mode==='manual');$('#mr').classList.toggle('on',Rm.mode==='random');[0,1,2].forEach(i=>$('#m'+i).classList.toggle('on',Rm.map===i))}
|
| 450 |
+
document.addEventListener('click',e=>{const b=e.target.closest('[data-a]');if(!b)return;const[a,v]=b.dataset.a.split(':');ac();
|
| 451 |
+
if(a==='team')send({t:'team',team:v});else if(a==='mode')send({t:'mode',m:v});else if(a==='map')send({t:'map',m:+v});else if(a==='start')send({t:'start'});
|
| 452 |
+
else if(a==='snd'){muted=!muted;setSnd()}
|
| 453 |
+
else if(a==='share'&&Rm){const u='https://t.me/share/url?url='+encodeURIComponent(Rm.link)+'&text='+encodeURIComponent('تعال العب CALL OF DUSY معي! كود الغرفة '+Rm.code);TG?TG.openTelegramLink(u):window.open(u)}
|
| 454 |
else if(a==='copy'&&Rm){try{navigator.clipboard.writeText(Rm.link);note('تم نسخ الرابط')}catch(_){note(Rm.link)}}});
|
| 455 |
+
/* ---------- three.js setup ---------- */
|
| 456 |
+
const renderer=new THREE.WebGLRenderer({canvas:$('#cv'),antialias:!touch}),scene=new THREE.Scene(),cam=new THREE.PerspectiveCamera(75,1,.1,600);cam.rotation.order='YXZ';scene.add(cam);
|
| 457 |
+
renderer.autoClear=false;
|
| 458 |
+
const hemi=new THREE.HemisphereLight(0xffffff,0x556677,.9),sun=new THREE.DirectionalLight(0xffffff,.7);scene.add(hemi,sun);
|
| 459 |
+
const wScene=new THREE.Scene(),wCam=new THREE.PerspectiveCamera(58,1,.02,10),wHemi=new THREE.HemisphereLight(0xffffff,0x555566,.85),wSun=new THREE.DirectionalLight(0xffffff,1);wSun.position.set(1,1.6,1.2);wScene.add(wHemi,wSun);
|
| 460 |
+
let baseFov=75;function fit(){renderer.setPixelRatio(Math.min(devicePixelRatio||1,1.6));renderer.setSize(innerWidth,innerHeight,false);cam.aspect=wCam.aspect=innerWidth/innerHeight;baseFov=cam.aspect<1?92:75;cam.updateProjectionMatrix();wCam.updateProjectionMatrix()}addEventListener('resize',fit);fit();
|
| 461 |
+
const mkL=c=>new THREE.MeshLambertMaterial({color:c});
|
| 462 |
+
/* ---------- procedural textures ---------- */
|
| 463 |
+
const TC={};
|
| 464 |
+
function tex(name){if(TC[name])return TC[name];const S=256,c=document.createElement('canvas');c.width=c.height=S;const x=c.getContext('2d'),rnd=(a,b)=>a+Math.random()*(b-a);
|
| 465 |
+
const fill=col=>{x.fillStyle=col;x.fillRect(0,0,S,S)},speck=(n,cols,s0,s1)=>{for(let i=0;i<n;i++){x.fillStyle=cols[i%cols.length];x.globalAlpha=rnd(.08,.32);const s=rnd(s0,s1);x.fillRect(rnd(0,S),rnd(0,S),s,s)}x.globalAlpha=1};
|
| 466 |
+
const line=(a,b,c2,d,col,w)=>{x.strokeStyle=col;x.lineWidth=w;x.beginPath();x.moveTo(a,b);x.lineTo(c2,d);x.stroke()};
|
| 467 |
+
switch(name){
|
| 468 |
+
case'sand':fill('#c79a5c');speck(2800,['#e0b573','#a97b45','#d6a867'],1,4);x.strokeStyle='rgba(120,80,40,.13)';x.lineWidth=2;for(let i=0;i<14;i++){x.beginPath();x.moveTo(0,i*19+rnd(-4,4));for(let u=0;u<=S;u+=32)x.lineTo(u,i*19+rnd(-5,5));x.stroke()}break;
|
| 469 |
+
case'sandstone':fill('#b58a5a');for(let r=0;r<8;r++){const off=(r%2)*32;for(let u=-off;u<S;u+=64){x.fillStyle=['#b98e5e','#a8804f','#c49a68','#b08556'][(r*3+(u>>6)+4)%4];x.fillRect(u+1,r*32+1,62,30)}}speck(1000,['#8a6338','#d9b585'],1,3);for(let r=0;r<=8;r++)line(0,r*32,S,r*32,'rgba(60,35,15,.5)',2);for(let r=0;r<8;r++){const off=(r%2)*32;for(let u=-off;u<=S;u+=64)line(u,r*32,u,r*32+32,'rgba(60,35,15,.5)',2)}break;
|
| 470 |
+
case'woodcrate':fill('#8b5a2b');for(let i=0;i<5;i++){x.fillStyle=i%2?'#94622f':'#80502a';x.fillRect(0,i*51,S,49);line(0,i*51+50,S,i*51+50,'#3d2410',2)}speck(800,['#5c3a18','#b0803f'],1,3);x.strokeStyle='#4a2d12';x.lineWidth=16;x.strokeRect(8,8,S-16,S-16);line(14,14,S-14,S-14,'#5b3a18',14);line(S-14,14,14,S-14,'#5b3a18',14);x.fillStyle='#caa46a';for(const[a,b]of[[22,22],[S-22,22],[22,S-22],[S-22,S-22]]){x.beginPath();x.arc(a,b,4,0,7);x.fill()}break;
|
| 471 |
+
case'snow':fill('#e9f0f7');speck(3200,['#ffffff','#c9d8e8','#d9e6f2'],1,3);break;
|
| 472 |
+
case'panel':fill('#7d8fa3');for(let a=0;a<2;a++)for(let b=0;b<2;b++){x.fillStyle=(a+b)%2?'#8799ad':'#74879b';x.fillRect(a*128+3,b*128+3,122,122);x.fillStyle='#4b5a6b';for(const[u,v]of[[12,12],[116,12],[12,116],[116,116]]){x.beginPath();x.arc(a*128+u,b*128+v,3.5,0,7);x.fill()}}for(let i=0;i<=2;i++){line(i*128,0,i*128,S,'#38465a',4);line(0,i*128,S,i*128,'#38465a',4)}x.fillStyle='#e6b21f';for(let i=0;i<8;i++){x.beginPath();x.moveTo(i*32,S-18);x.lineTo(i*32+16,S-18);x.lineTo(i*32+8,S-4);x.lineTo(i*32-8,S-4);x.fill()}speck(900,['#5a6a7c','#a9b9c9'],1,3);break;
|
| 473 |
+
case'icecrate':fill('#5b86b0');speck(900,['#a9d4ff','#3f6a96'],1,3);x.strokeStyle='#cfe6ff';x.lineWidth=16;x.strokeRect(8,8,S-16,S-16);line(14,14,S-14,S-14,'#8fb8e0',12);line(S-14,14,14,S-14,'#8fb8e0',12);x.fillStyle='#fff';for(const[a,b]of[[22,22],[S-22,22],[22,S-22],[S-22,S-22]]){x.beginPath();x.arc(a,b,4,0,7);x.fill()}break;
|
| 474 |
+
case'asphalt':fill('#2c2c36');speck(3500,['#3b3b48','#1f1f27','#4a4a58'],1,3);for(let i=0;i<4;i++){x.strokeStyle='rgba(0,0,0,.45)';x.lineWidth=1.5;x.beginPath();let px=rnd(0,S),py=rnd(0,S);x.moveTo(px,py);for(let k=0;k<6;k++){px+=rnd(-22,22);py+=rnd(-22,22);x.lineTo(px,py)}x.stroke()}break;
|
| 475 |
+
case'concrete':fill('#6b6a7a');speck(2600,['#7c7b8c','#585768','#8a8a99'],1,4);for(let i=0;i<=2;i++){line(i*128,0,i*128,S,'rgba(20,18,35,.55)',3);line(0,i*128,S,i*128,'rgba(20,18,35,.55)',3)}x.fillStyle='rgba(0,0,0,.12)';for(let i=0;i<5;i++)x.fillRect(rnd(0,S),rnd(0,S),rnd(6,18),rnd(30,90));break;
|
| 476 |
+
case'container':case'container2':{const c1=name==='container'?['#a63d40','#bd4a4d','#6e2224']:['#2f6b9a','#3f80b3','#1d4566'];fill(c1[0]);for(let u=0;u<S;u+=16){x.fillStyle=c1[1];x.fillRect(u,0,7,S);x.fillStyle='rgba(0,0,0,.25)';x.fillRect(u+9,0,3,S)}x.strokeStyle=c1[2];x.lineWidth=14;x.strokeRect(7,7,S-14,S-14);speck(700,['#000','#fff'],1,3);break}
|
| 477 |
+
case'win':fill('#0e0d22');for(let a=0;a<8;a++)for(let b=0;b<8;b++){const r=Math.random();x.fillStyle=r<.38?['#ffd27a','#8fe9ff','#ff9ad5','#fff1b8'][(Math.random()*4)|0]:'#1b1838';x.fillRect(a*32+6,b*32+6,20,20)}break}
|
| 478 |
+
const t=new THREE.CanvasTexture(c);t.wrapS=t.wrapT=THREE.RepeatWrapping;t.anisotropy=4;return TC[name]=t}
|
| 479 |
+
const MC={};function matFor(n,v){const k=n+v;return MC[k]||(MC[k]=new THREE.MeshLambertMaterial({map:tex(n),color:[0xffffff,0xe8e0d6,0xd6cec6][v]}))}
|
| 480 |
+
function boxG(w,h,d,t){const g=new THREE.BoxGeometry(w,h,d),uv=g.attributes.uv,D=[[d,h],[d,h],[w,d],[w,d],[w,h],[w,h]];for(let f=0;f<6;f++)for(let i=0;i<4;i++){const k=f*4+i;uv.setXY(k,uv.getX(k)*D[f][0]/t,uv.getY(k)*D[f][1]/t)}return g}
|
| 481 |
+
/* ---------- maps / sky / decor ---------- */
|
| 482 |
+
const MAPS=[
|
| 483 |
+
{sky:['#24226b','#a74e84','#ff9d58','#ffcf8a'],fog:0xdf9764,fd:[22,120],hemi:[0xffd9b0,0x7a5a3a,.95],sun:[0xffc27a,1,[-30,40,-40]],gr:'sand',og:0x9a6a40,wall:'sandstone',block:'sandstone',crate:['woodcrate'],orb:{c:'#ffb266',d:[-.55,.13,-.82],s:90,core:.2},stars:.3,fx:{c:0xf0c890,s:.12,v:[1.6,-.15,.6],o:.55,n:260}},
|
| 484 |
+
{sky:['#16254f','#5a72ae','#c9b6d6','#e9dfe9'],fog:0xaeb8d8,fd:[20,110],hemi:[0xdbe8ff,0x8095b5,1.05],sun:[0xe6eeff,.75,[25,45,15]],gr:'snow',og:0xcfd8e8,wall:'panel',block:'panel',crate:['icecrate','panel'],orb:{c:'#f0f4ff',d:[.4,.5,-.8],s:44,core:.3},stars:1,fx:{c:0xffffff,s:.14,v:[.6,-2.2,.3],o:.9,n:520}},
|
| 485 |
+
{sky:['#050512','#17104a','#6a2a78','#c4506a'],fog:0x2d1d4f,fd:[18,125],hemi:[0x7f7ee0,0x251f3f,.9],sun:[0xa8b6ff,.6,[-20,40,20]],gr:'asphalt',og:0x1b1a2b,wall:'concrete',block:'concrete',crate:['container','container2'],orb:{c:'#f4f0ff',d:[-.3,.45,-.85],s:40,core:.28},stars:1,fx:{c:0xffa066,s:.1,v:[.2,.5,.2],o:.85,n:240}}];
|
| 486 |
+
let world=new THREE.Group(),skyG=new THREE.Group(),parts=null,partCfg=null;scene.add(world,skyG);
|
| 487 |
+
function glowTex(col,core){const c=document.createElement('canvas');c.width=c.height=128;const x=c.getContext('2d'),g=x.createRadialGradient(64,64,0,64,64,64);g.addColorStop(0,col);g.addColorStop(core,col);g.addColorStop(core+.04,col+'66');g.addColorStop(1,'rgba(0,0,0,0)');x.fillStyle=g;x.fillRect(0,0,128,128);return new THREE.CanvasTexture(c)}
|
| 488 |
+
function buildSky(C){scene.remove(skyG);skyG.traverse(o=>{if(o.geometry)o.geometry.dispose();if(o.material){if(o.material.map)o.material.map.dispose();o.material.dispose()}});skyG=new THREE.Group();
|
| 489 |
+
const c=document.createElement('canvas');c.width=4;c.height=256;const x=c.getContext('2d'),g=x.createLinearGradient(0,0,0,256),fh='#'+C.fog.toString(16).padStart(6,'0');
|
| 490 |
+
g.addColorStop(0,C.sky[0]);g.addColorStop(.28,C.sky[1]);g.addColorStop(.44,C.sky[2]);g.addColorStop(.5,C.sky[3]);g.addColorStop(.505,fh);g.addColorStop(1,fh);x.fillStyle=g;x.fillRect(0,0,4,256);
|
| 491 |
+
const dome=new THREE.Mesh(new THREE.SphereGeometry(300,24,16),new THREE.MeshBasicMaterial({map:new THREE.CanvasTexture(c),side:THREE.BackSide,fog:false,depthWrite:false}));dome.renderOrder=-3;skyG.add(dome);
|
| 492 |
+
const o=C.orb,d=new THREE.Vector3(...o.d).normalize().multiplyScalar(280),sp=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex(o.c,o.core),transparent:true,fog:false,depthWrite:false}));sp.scale.set(o.s*2.4,o.s*2.4,1);sp.position.copy(d);sp.renderOrder=-2;skyG.add(sp);
|
| 493 |
+
const n=Math.floor(450*C.stars),pos=new Float32Array(n*3);for(let i=0;i<n;i++){const a=Math.random()*6.283,y=.06+Math.random()*.94,r=Math.sqrt(1-y*y)*285;pos[i*3]=Math.cos(a)*r;pos[i*3+1]=y*285;pos[i*3+2]=Math.sin(a)*r}
|
| 494 |
+
const sg=new THREE.BufferGeometry();sg.setAttribute('position',new THREE.BufferAttribute(pos,3));const st=new THREE.Points(sg,new THREE.PointsMaterial({color:0xffffff,size:1.7,sizeAttenuation:false,fog:false,transparent:true,opacity:.85,depthWrite:false}));st.renderOrder=-2;skyG.add(st);scene.add(skyG)}
|
| 495 |
+
function mkParts(C){if(parts){scene.remove(parts);parts.geometry.dispose()}const f=C.fx,pos=new Float32Array(f.n*3);for(let k=0;k<f.n;k++){pos[k*3]=Math.random()*60-30;pos[k*3+1]=Math.random()*22;pos[k*3+2]=Math.random()*60-30}
|
| 496 |
+
const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.BufferAttribute(pos,3));parts=new THREE.Points(g,new THREE.PointsMaterial({color:f.c,size:f.s,transparent:true,opacity:f.o,depthWrite:false}));parts.frustumCulled=false;partCfg=f;scene.add(parts)}
|
| 497 |
+
function updParts(dt){if(!parts)return;const a=parts.geometry.attributes.position,v=partCfg.v,cx=cam.position.x,cz=cam.position.z;for(let k=0;k<a.count;k++){let x=a.getX(k)+v[0]*dt,y=a.getY(k)+v[1]*dt,z=a.getZ(k)+v[2]*dt;if(x>cx+30)x-=60;else if(x<cx-30)x+=60;if(z>cz+30)z-=60;else if(z<cz-30)z+=60;if(y<0)y+=22;else if(y>22)y-=22;a.setXYZ(k,x,y,z)}a.needsUpdate=true}
|
| 498 |
+
function pine(x,z,s){const g=new THREE.Group();const tr=new THREE.Mesh(new THREE.CylinderGeometry(.35*s,.5*s,3*s,6),mkL(0x4a3426));tr.position.y=1.5*s;g.add(tr);for(let i=0;i<4;i++){const c=new THREE.Mesh(new THREE.ConeGeometry((3.6-i*.7)*s,(3.6-i*.3)*s,8),mkL(i%2?0x2d5a45:0x244c3a));c.position.y=(3+i*2.1)*s;g.add(c);const sn=new THREE.Mesh(new THREE.ConeGeometry((2.6-i*.6)*s,1.4*s,8),mkL(0xf2f6fb));sn.position.y=(4.1+i*2.1)*s;g.add(sn)}g.position.set(x,0,z);return g}
|
| 499 |
+
function buildWorld(i){scene.remove(world);world.traverse(o=>{if(o.geometry)o.geometry.dispose()});world=new THREE.Group();scene.add(world);const C=MAPS[i],rnd=(a,b)=>a+Math.random()*(b-a);
|
| 500 |
+
scene.background=new THREE.Color(C.fog);scene.fog=new THREE.Fog(C.fog,C.fd[0],C.fd[1]);
|
| 501 |
+
for(const[h,s,w]of[[hemi,sun,0],[wHemi,wSun,1]]){h.color.setHex(C.hemi[0]);h.groundColor.setHex(C.hemi[1]);h.intensity=w?C.hemi[2]*.85:C.hemi[2];s.color.setHex(C.sun[0]);s.intensity=w?1:C.sun[1]}sun.position.set(...C.sun[2]);
|
| 502 |
+
buildSky(C);mkParts(C);
|
| 503 |
+
const gt=tex(C.gr);gt.repeat.set(20,20);const g=new THREE.Mesh(new THREE.PlaneGeometry(80,80),new THREE.MeshLambertMaterial({map:gt}));g.rotation.x=-Math.PI/2;world.add(g);
|
| 504 |
+
const og=new THREE.Mesh(new THREE.PlaneGeometry(900,900),mkL(C.og));og.rotation.x=-Math.PI/2;og.position.y=-.03;world.add(og);
|
| 505 |
+
boxes.forEach((b,k)=>{const[x,y0,z,w,h,d]=b,crate=w<=4.5&&Math.abs(w-d)<=.7&&h<=2.2&&w>=1.9,n=crate?C.crate[k%C.crate.length]:(h>=6?C.wall:C.block),m=new THREE.Mesh(boxG(w,h,d,crate?2:3),matFor(n,k%3));m.position.set(x,y0+h/2,z);world.add(m)});
|
| 506 |
+
if(i===0){for(let k=0;k<16;k++){const a=k/16*6.283+rnd(0,.3),r=rnd(85,125),m=new THREE.Mesh(new THREE.SphereGeometry(1,14,8),mkL(0xb7824a));m.scale.set(rnd(28,60),rnd(7,16),rnd(28,55));m.position.set(Math.cos(a)*r,-2,Math.sin(a)*r);world.add(m)}
|
| 507 |
+
for(let k=0;k<8;k++){const a=rnd(0,6.283),r=rnd(78,105),w=rnd(10,18),h=rnd(14,28),m=new THREE.Mesh(new THREE.BoxGeometry(w,h,w*rnd(.6,1.1)),matFor('sandstone',1));m.position.set(Math.cos(a)*r,h/2-1,Math.sin(a)*r);m.rotation.y=rnd(0,3);world.add(m)}}
|
| 508 |
+
else if(i===1){for(let k=0;k<12;k++){const a=k/12*6.283+rnd(0,.3),r=rnd(110,150),R=rnd(26,44),H=rnd(40,70),m=new THREE.Mesh(new THREE.ConeGeometry(R,H,7),mkL(0x7f8da6)),cap=new THREE.Mesh(new THREE.ConeGeometry(R*.42,H*.42,7),mkL(0xf4f7fb));m.position.set(Math.cos(a)*r,H/2-2,Math.sin(a)*r);cap.position.set(m.position.x,H*.79-2,m.position.z);world.add(m,cap)}
|
| 509 |
+
for(let k=0;k<46;k++){const a=rnd(0,6.283),r=rnd(42,75);world.add(pine(Math.cos(a)*r,Math.sin(a)*r,rnd(.9,1.6)))}}
|
| 510 |
+
else{const wm=[];for(let k=0;k<4;k++){const t=tex('win').clone();t.needsUpdate=true;wm.push(new THREE.MeshLambertMaterial({map:t,emissive:0xffffff,emissiveMap:t}))}
|
| 511 |
+
const tall=[];for(let k=0;k<48;k++){const a=k/48*6.283+rnd(-.05,.05),r=rnd(52,100),w=rnd(8,18),d=rnd(8,18),h=rnd(18,75),m=new THREE.Mesh(boxG(w,h,d,8),wm[k%4]);m.position.set(Math.cos(a)*r,h/2-1,Math.sin(a)*r);m.rotation.y=rnd(-.2,.2);world.add(m);if(h>58)tall.push([m.position.x,h,m.position.z])}
|
| 512 |
+
const bm=new THREE.MeshBasicMaterial({color:0xff3344});tall.forEach(([x,h,z])=>{const s=new THREE.Mesh(new THREE.SphereGeometry(.8,6,6),bm);s.position.set(x,h-.4,z);world.add(s)})}}
|
| 513 |
+
/* ---------- weapon models ---------- */
|
| 514 |
+
const PH=(c,s,sh)=>new THREE.MeshPhongMaterial({color:c,specular:s==null?0x333333:s,shininess:sh||30});
|
| 515 |
+
const B3=(w,h,d)=>new THREE.BoxGeometry(w,h,d),cyl=(r1,r2,l,sg)=>{const g=new THREE.CylinderGeometry(r1,r2,l,sg||12);g.rotateX(Math.PI/2);return g};
|
| 516 |
+
function mk(g,geo,mat,x,y,z){const m=new THREE.Mesh(geo,mat);m.position.set(x||0,y||0,z||0);g.add(m);return m}
|
| 517 |
+
function limb(g,a,b,w,mat){const d=new THREE.Vector3(...b).sub(new THREE.Vector3(...a)),l=d.length(),m=new THREE.Mesh(B3(w,w,l),mat);m.position.set((a[0]+b[0])/2,(a[1]+b[1])/2,(a[2]+b[2])/2);m.quaternion.setFromUnitVectors(new THREE.Vector3(0,0,1),d.normalize());g.add(m);return m}
|
| 518 |
+
const sleeveM=PH(0x1e5a55,0x111111,10),gloveM=PH(0x15161a,0x444444,25);
|
| 519 |
+
function makeKnife(fp){const g=new THREE.Group(),steel=PH(0xd8dfe6,0xffffff,140),dark=PH(0x1b1d22,0x555555,30),rub=PH(0x2a2d33,0x222222,10);
|
| 520 |
+
const sh=new THREE.Shape();sh.moveTo(0,.03);sh.lineTo(.2,.03);sh.quadraticCurveTo(.285,.03,.32,-.012);sh.quadraticCurveTo(.26,-.046,.19,-.04);sh.lineTo(0,-.032);sh.lineTo(0,.03);
|
| 521 |
+
const bg=new THREE.ExtrudeGeometry(sh,{depth:.009,bevelEnabled:false});bg.translate(0,0,-.0045);bg.rotateY(Math.PI/2);mk(g,bg,steel);
|
| 522 |
+
mk(g,B3(.0105,.009,.2),PH(0x8f99a3,0xffffff,60),0,.014,-.1); // fuller groove
|
| 523 |
+
mk(g,B3(.0105,.006,.17),PH(0xffffff,0xffffff,200),0,-.034,-.1); // sharpened edge highlight
|
| 524 |
+
mk(g,B3(.022,.09,.016),dark,0,0,.004);
|
| 525 |
+
mk(g,cyl(.017,.019,.135,10),rub,0,0,.0725);
|
| 526 |
+
for(let i=0;i<5;i++)mk(g,cyl(.0205,.0205,.006,10),dark,0,0,.025+i*.024);
|
| 527 |
+
mk(g,new THREE.SphereGeometry(.022,10,8),steel,0,0,.142);
|
| 528 |
+
if(fp){mk(g,B3(.074,.07,.1),gloveM,0,-.008,.075);mk(g,B3(.068,.022,.09),gloveM,0,.034,.075);mk(g,B3(.026,.026,.06),gloveM,-.038,.016,.03);limb(g,[0,-.015,.12],[.07,-.17,.7],.085,sleeveM)}
|
| 529 |
+
return g}
|
| 530 |
+
function makeSniper(fp){const g=new THREE.Group(),gm=PH(0x2a2e35,0x777b83,70),gm2=PH(0x1a1c21,0x444444,35),olive=PH(0x3b4636,0x222222,15),glass=new THREE.MeshPhongMaterial({color:0x3a7bd5,emissive:0x0a2a55,specular:0xffffff,shininess:160,transparent:true,opacity:.88});
|
| 531 |
+
mk(g,B3(.062,.085,.42),gm,0,0,-.02);mk(g,B3(.03,.012,.4),gm2,0,.048,-.02);
|
| 532 |
+
mk(g,cyl(.027,.027,.3,10),gm2,0,-.004,-.36);mk(g,cyl(.0135,.0135,.62,10),gm,0,0,-.66);
|
| 533 |
+
mk(g,cyl(.022,.022,.09,8),gm2,0,0,-.955);for(let i=0;i<3;i++)mk(g,B3(.056,.012,.01),gm,0,0,-.93-i*.024);
|
| 534 |
+
const st=mk(g,B3(.052,.095,.3),olive,0,-.012,.39);st.rotation.x=.05;mk(g,B3(.05,.04,.16),olive,0,.062,.34);mk(g,B3(.056,.115,.025),PH(0x15161a),0,-.016,.545);
|
| 535 |
+
const gr=mk(g,B3(.038,.1,.05),gm2,0,-.085,.17);gr.rotation.x=-.35;mk(g,B3(.034,.09,.075),gm2,0,-.082,-.06);
|
| 536 |
+
const tg=mk(g,new THREE.TorusGeometry(.03,.004,6,12),gm2,0,-.062,.05);tg.geometry.rotateY(Math.PI/2);
|
| 537 |
+
mk(g,cyl(.027,.027,.3,14),gm,0,.098,-.06);mk(g,cyl(.027,.042,.09,14),gm,0,.098,-.255);mk(g,cyl(.036,.027,.06,14),gm,0,.098,.125);
|
| 538 |
+
const ln=mk(g,new THREE.CircleGeometry(.038,16),glass,0,.098,-.3015);ln.rotation.y=Math.PI;
|
| 539 |
+
mk(g,new THREE.CylinderGeometry(.01,.01,.025,8),gm2,0,.128,-.06);const tr=mk(g,new THREE.CylinderGeometry(.01,.01,.025,8),gm2,.034,.098,-.06);tr.rotation.z=Math.PI/2;
|
| 540 |
+
mk(g,B3(.03,.04,.03),gm2,0,.07,-.15);mk(g,B3(.03,.04,.03),gm2,0,.07,.03);
|
| 541 |
+
const bolt=new THREE.Group();bolt.position.set(.045,.014,.04);const bh=new THREE.CylinderGeometry(.006,.006,.07,6);bh.rotateZ(Math.PI/2);mk(bolt,bh,gm,.035,0,0);mk(bolt,new THREE.SphereGeometry(.014,8,6),gm2,.073,0,0);g.add(bolt);
|
| 542 |
+
for(const s of[-1,1]){const l=mk(g,new THREE.CylinderGeometry(.005,.005,.14,5),gm2,s*.03,-.04,-.5);l.rotation.x=1.45;l.rotation.z=s*.12}
|
| 543 |
+
const flash=new THREE.Group();flash.position.z=-1.02;flash.visible=false;const fm=new THREE.MeshBasicMaterial({map:glowTex('#ffd27a',.1),transparent:true,blending:THREE.AdditiveBlending,depthWrite:false,side:THREE.DoubleSide});mk(flash,new THREE.PlaneGeometry(.34,.34),fm);const f2=mk(flash,new THREE.PlaneGeometry(.34,.34),fm);f2.rotation.y=Math.PI/2;g.add(flash);
|
| 544 |
+
if(fp){mk(g,B3(.07,.075,.09),gloveM,.002,-.1,.17);limb(g,[.005,-.1,.2],[.12,-.3,.75],.085,sleeveM);mk(g,B3(.08,.06,.1),gloveM,-.002,-.05,-.36);limb(g,[-.002,-.06,-.32],[-.2,-.34,.25],.085,sleeveM)}
|
| 545 |
+
g.userData={bolt,flash};return g}
|
| 546 |
+
/* first-person rigs (separate scene, drawn over the world) */
|
| 547 |
+
const kRig=new THREE.Group(),kModel=makeKnife(true),sRig=new THREE.Group(),sModel=makeSniper(true);kModel.rotation.set(.45,.4,-.25);kRig.add(kModel);sRig.add(sModel);wScene.add(kRig,sRig);
|
| 548 |
+
const bolt=sModel.userData.bolt,flash=sModel.userData.flash,boltZ=bolt.position.z,KB={x:.2,y:-.23,z:-.42},SB={x:.17,y:-.2,z:-.3};
|
| 549 |
+
let swapT=1,slashT=-1,fireT=-1,boltOk=true,bobT=0;
|
| 550 |
+
function wAnim(dt,sprint){swapT=Math.min(1,swapT+dt*3.2);const ax=Math.min(1,cam.aspect/1.25),e=1-Math.pow(1-swapT,3),amp=.3+mv*1.2;bobT+=dt*(5+mv*7);const bx=Math.sin(bobT)*.006*amp,by=Math.abs(Math.cos(bobT))*.006*amp;
|
| 551 |
+
let sl=0;if(slashT>=0){slashT+=dt/.34;if(slashT>=1)slashT=-1;else sl=Math.sin(slashT*Math.PI)}
|
| 552 |
+
kRig.position.set(KB.x*ax+bx-sl*.3,KB.y+by-(1-e)*.45+sl*.05,KB.z-sl*.2);kRig.rotation.set(sl*.55,sl*1.2,-sl*1.1);
|
| 553 |
+
let r=0;if(fireT>=0){fireT+=dt;r=Math.exp(-fireT*13);flash.visible=fireT<.07;flash.rotation.z=Math.random()*6;if(fireT>.45&&fireT<1){const q=(fireT-.45)/.55;if(!boltOk){boltOk=true;snd('bolt')}bolt.position.z=boltZ+Math.sin(q*Math.PI)*.06;bolt.rotation.z=-Math.sin(Math.min(1,q*1.1)*Math.PI)*1.0}else{bolt.position.z=boltZ;bolt.rotation.z=0}if(fireT>1.1){fireT=-1;flash.visible=false}}
|
| 554 |
+
sRig.position.set(SB.x*ax+bx*.6,SB.y+by*.6-(1-e)*.45,SB.z+r*.1);sRig.rotation.set(r*.12,.04,0)}
|
| 555 |
+
/* ---------- characters ---------- */
|
| 556 |
+
const skins=[0xf1c9a5,0xe0ac82,0xc68863,0x9a6a47,0x6f4a30],hairs=[0x1a1410,0x3a2616,0x6b4a2a,0x151515,0x8a8a8a],hex6=n=>'#'+n.toString(16).padStart(6,'0');
|
| 557 |
+
function hash(s){let h=0;for(const c of String(s))h=(h*31+c.charCodeAt(0))>>>0;return h}
|
| 558 |
+
const faceC={};
|
| 559 |
+
function faceTex(h){const k=h%15;if(faceC[k])return faceC[k];const si=k%5,hi=(k>>1)%5,beard=k%3===0,c=document.createElement('canvas');c.width=c.height=64;const x=c.getContext('2d'),sk=hex6(skins[si]),hr=hex6(hairs[hi]);
|
| 560 |
+
x.fillStyle=sk;x.fillRect(0,0,64,64);x.fillStyle='rgba(0,0,0,.08)';x.fillRect(0,50,64,14);
|
| 561 |
+
x.fillStyle=hr;x.fillRect(0,0,64,11);x.fillRect(0,0,5,24);x.fillRect(59,0,5,24);x.fillRect(11,19,16,3);x.fillRect(37,19,16,3);
|
| 562 |
+
x.fillStyle='#fff';x.fillRect(12,25,14,8);x.fillRect(38,25,14,8);x.fillStyle='#3a2414';x.fillRect(16,25,7,8);x.fillRect(42,25,7,8);x.fillStyle='#000';x.fillRect(18,26,3,6);x.fillRect(44,26,3,6);x.fillStyle='rgba(255,255,255,.9)';x.fillRect(19,26,1,1);x.fillRect(45,26,1,1);
|
| 563 |
+
x.fillStyle='rgba(0,0,0,.2)';x.fillRect(29,31,6,11);x.fillRect(26,40,12,3);
|
| 564 |
+
if(beard){x.fillStyle=hr;x.fillRect(7,40,50,24);x.fillRect(5,28,4,16);x.fillRect(55,28,4,16);x.fillStyle=sk;x.fillRect(23,47,18,6);x.fillStyle='#7a2e2e';x.fillRect(25,48,14,2)}else{x.fillStyle='#7a2e2e';x.fillRect(24,48,16,3);x.fillStyle='rgba(0,0,0,.14)';x.fillRect(22,46,2,5);x.fillRect(40,46,2,5)}
|
| 565 |
+
const t=new THREE.CanvasTexture(c);t.magFilter=THREE.NearestFilter;return faceC[k]=t}
|
| 566 |
+
const bxm=(par,w,h,d,mat,x,y,z)=>{const m=new THREE.Mesh(B3(w,h,d),mat);m.position.set(x,y,z);par.add(m);return m};
|
| 567 |
+
function avatar(p){const H=p.team==='H',tc=H?0x2fd6c0:0xff5a6e,td=H?0x146b62:0x8a2332,g=new THREE.Group(),body=new THREE.Group(),hh=hash(p.id||p.name);g.add(body);
|
| 568 |
+
const pants=mkL(H?0x2c3a3a:0x3a2c30),shirt=mkL(0x23262d),vest=mkL(td),vest2=mkL(0x1b1d22),glove=mkL(0x111114),boot=mkL(0x15151a),skin=mkL(skins[hh%5]);
|
| 569 |
+
const mkLeg=s=>{const pv=new THREE.Group();pv.position.set(s*.16,.88,0);bxm(pv,.24,.82,.28,pants,0,-.41,0);bxm(pv,.26,.16,.36,boot,0,-.8,-.04);bxm(pv,.27,.2,.29,vest2,0,-.3,0);body.add(pv);return pv};
|
| 570 |
+
const lL=mkLeg(-1),lR=mkLeg(1);
|
| 571 |
+
bxm(body,.6,.64,.34,shirt,0,1.2,0);bxm(body,.66,.5,.4,vest,0,1.26,0);bxm(body,.18,.14,.08,vest2,-.16,1.16,-.22);bxm(body,.18,.14,.08,vest2,.16,1.16,-.22);bxm(body,.1,.1,.05,mkL(tc),.17,1.4,-.21);bxm(body,.64,.08,.38,boot,0,.9,0);bxm(body,.3,.34,.12,vest2,0,1.3,.24);
|
| 572 |
+
const hd=new THREE.Group();hd.position.set(0,1.56,0);body.add(hd);bxm(hd,.12,.1,.12,skin,0,.04,0);
|
| 573 |
+
const fm=mkL(0xffffff);fm.map=faceTex(hh);const hairM=mkL(hairs[(hh>>1)%5]),head=new THREE.Mesh(B3(.32,.34,.32),[skin,skin,hairM,skin,hairM,fm]);head.position.y=.25;hd.add(head);
|
| 574 |
+
if(H){const hl=new THREE.Mesh(new THREE.SphereGeometry(.215,12,8,0,6.283,0,1.571),PH(0x1e8f84,0x88ffee,40));hl.scale.set(1,.95,1.08);hl.position.set(0,.33,0);hd.add(hl);bxm(hd,.34,.04,.3,PH(0x15665f),0,.325,-.02);
|
| 575 |
+
bxm(hd,.3,.075,.06,vest2,0,.37,-.19);const lm=new THREE.MeshBasicMaterial({color:0x66ffcc});for(const s of[-1,1]){const l=mk(hd,cyl(.032,.032,.07,8),lm,s*.08,.37,-.24)}}
|
| 576 |
+
else{const cp=new THREE.Mesh(new THREE.SphereGeometry(.2,12,8,0,6.283,0,1.571),mkL(0x8a1f2e));cp.scale.set(1,.8,1.05);cp.position.set(0,.33,0);hd.add(cp);bxm(hd,.3,.03,.16,mkL(0x6a1622),0,.335,-.2);bxm(hd,.36,.12,.36,mkL(tc),0,.03,0)}
|
| 577 |
+
const mkArm=s=>{const pv=new THREE.Group();pv.position.set(s*.4,1.46,0);bxm(pv,.16,.6,.16,shirt,0,-.3,0);bxm(pv,.19,.16,.19,vest,0,-.06,0);bxm(pv,.15,.13,.15,glove,0,-.64,0);body.add(pv);return pv};
|
| 578 |
+
const aL=mkArm(-1),aR=mkArm(1),kn=makeKnife(false),sn=makeSniper(false);kn.scale.setScalar(1.5);kn.rotation.x=-Math.PI/2;kn.position.set(0,-.64,0);aR.add(kn);sn.scale.setScalar(.92);sn.rotation.x=-Math.PI/2;sn.position.set(0,-.64-.17*.92,0);sn.userData.flash.visible=false;aR.add(sn);sn.visible=false;
|
| 579 |
+
const sh=new THREE.Mesh(new THREE.CircleGeometry(.55,16),new THREE.MeshBasicMaterial({color:0,transparent:true,opacity:.35,depthWrite:false}));sh.rotation.x=-Math.PI/2;sh.position.y=.03;g.add(sh);
|
| 580 |
+
let tag=null;if(p.team===myTeam){const c=document.createElement('canvas');c.width=256;c.height=64;const x=c.getContext('2d');x.font='bold 30px sans-serif';x.textAlign='center';x.fillStyle=TCOL[p.team];x.strokeStyle='#000';x.lineWidth=6;x.strokeText(p.name,128,42);x.fillText(p.name,128,42);tag=new THREE.Sprite(new THREE.SpriteMaterial({map:new THREE.CanvasTexture(c),depthTest:false,transparent:true}));tag.scale.set(2.2,.55,1);tag.position.y=2.45;tag.renderOrder=5;g.add(tag)}
|
| 581 |
+
scene.add(g);return{g,body,hd,lL,lR,aL,aR,kn,sn,team:p.team,ph:0,sp:0,dt:9,alive:false,wp:'k',pitch:0,t:[0,0,0]}}
|
| 582 |
+
function animOthers(dt){for(const k in others){const o=others[k],p=o.g.position,ox=p.x,oz=p.z;p.x+=(o.t[0]-p.x)*.3;p.y+=(o.t[1]-p.y)*.3;p.z+=(o.t[2]-p.z)*.3;
|
| 583 |
+
if(!o.alive){o.dt+=dt;o.body.rotation.x=Math.min(1,o.dt*3)*1.5;o.g.visible=o.dt<2.2;continue}o.g.visible=true;o.body.rotation.x=0;
|
| 584 |
+
const spd=Math.hypot(p.x-ox,p.z-oz)/Math.max(dt,.001);o.sp+=(spd-o.sp)*.2;const m=Math.min(1,o.sp/4);o.ph+=dt*(4+o.sp*1.3);const sw=Math.sin(o.ph)*.85*m,s=o.wp==='s',up=o.pitch;
|
| 585 |
+
o.lL.rotation.x=sw;o.lR.rotation.x=-sw;o.kn.visible=!s;o.sn.visible=s;o.hd.rotation.x=up*.7;o.body.position.y=Math.abs(Math.sin(o.ph))*.04*m;
|
| 586 |
+
o.aR.rotation.set(s?1.5+up:1.25+up*.8,0,s?-.1:0);if(s)o.aL.rotation.set(1.45+up,0,.42);else o.aL.rotation.set(-sw*.8,0,.05)}}
|
| 587 |
+
/* ---------- tracers ---------- */
|
| 588 |
+
const TRG=new THREE.CylinderGeometry(.02,.02,1,5),tracers=[];
|
| 589 |
+
function tracer(o,dv,mine){const s=o.clone().addScaledVector(dv,mine?.9:1.2),e=o.clone().addScaledVector(dv,95);if(mine){s.x+=Math.cos(P.yaw)*.16;s.z-=Math.sin(P.yaw)*.16;s.y-=.13}
|
| 590 |
+
const dir=e.sub(s),len=dir.length();dir.normalize();const mat=new THREE.MeshBasicMaterial({color:0xffe3a0,transparent:true,opacity:.9,fog:false,depthWrite:false}),m=new THREE.Mesh(TRG,mat);m.scale.set(1,len,1);m.position.copy(s).addScaledVector(dir,len/2);m.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),dir);scene.add(m);tracers.push({m,mat,t:0})}
|
| 591 |
+
function updTracers(dt){for(let i=tracers.length-1;i>=0;i--){const t=tracers[i];t.t+=dt;t.mat.opacity=Math.max(0,.9*(1-t.t/.28));if(t.t>=.28){scene.remove(t.m);t.mat.dispose();tracers.splice(i,1)}}}
|
| 592 |
+
/* ---------- radar ---------- */
|
| 593 |
+
const rdr=$('#radar'),rx=rdr.getContext('2d');
|
| 594 |
+
function radar(){const S=200,c=S/2,sc=c/32;rx.clearRect(0,0,S,S);rx.save();rx.beginPath();rx.arc(c,c,c-2,0,7);rx.clip();rx.fillStyle='rgba(12,7,28,.68)';rx.fillRect(0,0,S,S);
|
| 595 |
+
const cy=Math.cos(P.yaw),sy=Math.sin(P.yaw),tr=(wx,wz)=>{const dx=wx-P.x,dz=wz-P.z;return[c+(dx*cy-dz*sy)*sc,c+(dx*sy+dz*cy)*sc]};
|
| 596 |
+
rx.fillStyle='rgba(255,190,120,.3)';rx.strokeStyle='rgba(255,205,150,.7)';rx.lineWidth=2;
|
| 597 |
+
for(const[x,y0,z,w,h,d]of boxes){if(Math.abs(x-P.x)>w/2+34||Math.abs(z-P.z)>d/2+34)continue;rx.beginPath();[[x-w/2,z-d/2],[x+w/2,z-d/2],[x+w/2,z+d/2],[x-w/2,z+d/2]].forEach(([a,b],i)=>{const[u,v]=tr(a,b);i?rx.lineTo(u,v):rx.moveTo(u,v)});rx.closePath();rx.fill();rx.stroke()}
|
| 598 |
+
for(const k in others){const o=others[k];if(o.team!==myTeam||!o.alive)continue;const[u,v]=tr(o.g.position.x,o.g.position.z);rx.fillStyle=TCOL[myTeam];rx.beginPath();rx.arc(u,v,7,0,7);rx.fill();rx.strokeStyle='#000';rx.lineWidth=2;rx.stroke()}
|
| 599 |
+
rx.fillStyle='#fff';rx.strokeStyle='#000';rx.lineWidth=2;rx.beginPath();rx.moveTo(c,c-13);rx.lineTo(c+9,c+10);rx.lineTo(c,c+5);rx.lineTo(c-9,c+10);rx.closePath();rx.fill();rx.stroke();rx.restore()}
|
| 600 |
+
/* ---------- game flow ---------- */
|
| 601 |
+
function snap(m){lastSc=m.sc;$('#sH').textContent=m.sc[0];$('#sV').textContent=m.sc[1];$('#bH').style.width=Math.min(100,m.sc[0]/WIN*100)+'%';$('#bV').style.width=Math.min(100,m.sc[1]/WIN*100)+'%';
|
| 602 |
+
const clk=$('#clk');clk.textContent=Math.floor(m.left/60)+':'+String(m.left%60).padStart(2,'0');clk.classList.toggle('lo',m.left<=30);
|
| 603 |
+
for(const[id,x,y,z,yaw,al,pitch,w]of m.p){if(id===me)continue;let o=others[id];if(!o){const pl=Rm.players.find(p=>p.id===id);if(!pl)continue;o=others[id]=avatar(pl);o.g.position.set(x,y,z)}
|
| 604 |
+
if(al&&!o.alive){o.g.position.set(x,y,z);o.dt=0}if(!al&&o.alive)o.dt=0;o.alive=!!al;o.t=[x,y,z];o.g.rotation.y=yaw;o.pitch=pitch||0;o.wp=w||'k'}}
|
| 605 |
+
function endGame(){playing=false;alive=false;scoped=false;fireHeld=false;for(const k in others){scene.remove(others[k].g)}others={};for(const t of tracers){scene.remove(t.m)}tracers.length=0;show('#over',0);show('#death',0);show('#scope',0);show('#cdn',0)}
|
| 606 |
+
function startGame(m){endGame();boxes=m.boxes;mapI=m.map;myTeam=m.team;WIN=m.win||25;kills=0;deaths=0;updKD();lastSc=[0,0];buildWorld(m.map);sleeveM.color.setHex(myTeam==='H'?0x146b62:0x8a2332);
|
| 607 |
+
show('#lobby',0);show('#hud',1);show('#over',0);$('#feed').innerHTML='';$('#sH').textContent=$('#sV').textContent='0';$('#bH').style.width=$('#bV').style.width='0';playing=true;goAt=performance.now()+m.cd*1000;lastCd=0;wp='s';setWp('k');swapT=0;
|
| 608 |
+
hintT=performance.now()+9000;$('#hint').style.opacity=.85;$('#hint').textContent=touch?'عصا يسار للحركة • اسحب باليمين للتصويب • 🔪 خفيف وسريع • 🎯 سنايبر قاتل':'WASD حركة • ماوس تصويب • كليك يسار ضرب • كليك يمين منظار • 1/2 تبديل السلاح • Space قفز'}
|
| 609 |
+
function setWp(w){if(w!==wp){swapT=0;snd('swap')}wp=w;if(w==='k')scoped=false;kRig.visible=wp==='k';sRig.visible=wp==='s'&&!scoped;show('#scope',scoped?1:0);show('#xh',scoped?0:1);$('#bs').style.visibility=wp==='s'?'visible':'hidden';
|
| 610 |
+
$('#s1').classList.toggle('on',wp==='k');$('#s2').classList.toggle('on',wp==='s');$('#bf').innerHTML=wp==='k'?IC.k:IC.cross;$('#cdbar').style.opacity=0}
|
| 611 |
+
function toggleScope(){if(wp==='s'&&alive){scoped=!scoped;setWp('s');snd('swap')}}
|
| 612 |
+
function doFire(){const n=performance.now();if(!alive||n<goAt||n<cd)return;const k=wp==='k';cdLen=k?450:1600;cd=n+cdLen;cdStart=n;send({t:'fire',w:wp});
|
| 613 |
+
if(k){slashT=0;snd('knife');hap('light')}else{fireT=0;boltOk=false;P.pitch=Math.min(1.45,P.pitch+.014);snd('snipe');hap('heavy')}}
|
| 614 |
function look(dx,dy){const s=scoped?.0013:.0036;P.yaw-=dx*s;P.pitch=Math.max(-1.45,Math.min(1.45,P.pitch-dy*s))}
|
| 615 |
+
const PR=.4;
|
| 616 |
+
function move(dt){let fx=0,fz=0;if(keys.KeyW)fz--;if(keys.KeyS)fz++;if(keys.KeyA)fx--;if(keys.KeyD)fx++;fx+=joy.x;fz+=joy.y;const l=Math.hypot(fx,fz);if(l>1){fx/=l;fz/=l}mv=Math.min(1,l);
|
| 617 |
const sp=wp==='k'?7:scoped?2.2:4.8,s=Math.sin(P.yaw),c=Math.cos(P.yaw);let nx=P.x+(fx*c+fz*s)*sp*dt,nz=P.z+(-fx*s+fz*c)*sp*dt;
|
| 618 |
+
for(const[x,y0,z,w,h,d]of boxes){if(P.y<y0+h-.55&&P.y+1.7>y0){const ox=w/2+PR-Math.abs(nx-x),oz=d/2+PR-Math.abs(nz-z);if(ox>0&&oz>0){if(ox<oz)nx+=Math.sign(nx-x||1)*ox;else nz+=Math.sign(nz-z||1)*oz}}}
|
| 619 |
P.x=Math.max(-34,Math.min(34,nx));P.z=Math.max(-34,Math.min(34,nz));let gr=0;
|
| 620 |
+
for(const[x,y0,z,w,h,d]of boxes){const top=y0+h;if(Math.abs(P.x-x)<w/2+PR-.02&&Math.abs(P.z-z)<d/2+PR-.02&&P.y>=top-.55&&top>gr)gr=top}
|
| 621 |
+
if(keys.Space&&P.ground){P.vy=9}P.vy-=25*dt;P.y+=P.vy*dt;if(P.y<=gr){P.y=gr;P.vy=0;P.ground=true}else P.ground=false}
|
| 622 |
+
let last=performance.now(),lastSend=0,fr=0;
|
| 623 |
+
function frame(n){requestAnimationFrame(frame);const dt=Math.min((n-last)/1000,.05);last=n;fr++;
|
| 624 |
+
if(playing){
|
| 625 |
+
if(n<goAt){const c=Math.ceil((goAt-n)/1000);$('#cdn').innerHTML=c+'<small>استعد!</small>';show('#cdn','flex');if(c!==lastCd){lastCd=c;snd('tick')}}else{if(lastCd){lastCd=0;snd('go');$('#cdn').innerHTML='ابدأ!';setTimeout(()=>show('#cdn',0),700)}}
|
| 626 |
+
mv=0;if(alive&&n>=goAt){move(dt);if(fireHeld&&wp==='k')doFire();if(n-lastSend>50){lastSend=n;send({t:'st',x:P.x,y:P.y,z:P.z,yaw:P.yaw,pitch:P.pitch,wp})}if(P.y<-12){send({t:'fall'});alive=false}}
|
| 627 |
+
if(!alive&&deathEnd){const el=$('#dsec');if(el)el.textContent=Math.max(0,Math.ceil((deathEnd-n)/1000))}
|
| 628 |
+
cam.position.set(P.x,P.y+1.6,P.z);cam.rotation.set(P.pitch,P.yaw,0);const tf=scoped?16:baseFov;cam.fov+=(tf-cam.fov)*Math.min(1,dt*14);cam.updateProjectionMatrix();
|
| 629 |
+
skyG.position.copy(cam.position);wAnim(dt);updParts(dt);updTracers(dt);animOthers(dt);
|
| 630 |
+
if(fr%3===0)radar();
|
| 631 |
+
const cdb=$('#cdbar');if(wp==='s'&&n<cdStart+cdLen&&alive){cdb.style.opacity=1;cdb.firstChild.style.width=((n-cdStart)/cdLen*100)+'%'}else if(cdb.style.opacity!=='0')cdb.style.opacity=0;
|
| 632 |
+
if(hintT&&n>hintT){hintT=0;$('#hint').style.opacity=0}
|
| 633 |
+
renderer.clear();renderer.render(scene,cam);if(alive&&!scoped){renderer.clearDepth();renderer.render(wScene,wCam)}}}
|
| 634 |
requestAnimationFrame(frame);
|
| 635 |
+
/* ---------- input ---------- */
|
| 636 |
+
const cv=$('#cv');cv.addEventListener('click',()=>{if(!touch&&playing&&alive&&cv.requestPointerLock)cv.requestPointerLock()});
|
|
|
|
|
|
|
|
|
|
| 637 |
document.addEventListener('mousemove',e=>{if(document.pointerLockElement===cv&&playing&&alive)look(e.movementX,e.movementY)});
|
| 638 |
cv.addEventListener('mousedown',e=>{if(document.pointerLockElement!==cv)return;if(e.button===0){fireHeld=true;doFire()}if(e.button===2)toggleScope()});
|
| 639 |
addEventListener('mouseup',e=>{if(e.button===0)fireHeld=false});cv.addEventListener('contextmenu',e=>e.preventDefault());
|
| 640 |
+
addEventListener('keydown',e=>{keys[e.code]=true;if(!playing)return;if(e.code==='Digit1')setWp('k');if(e.code==='Digit2')setWp('s');if(e.code==='KeyQ')setWp(wp==='k'?'s':'k')});addEventListener('keyup',e=>keys[e.code]=false);
|
| 641 |
let joyId=null,joyO=null,lookId=null,lx=0,ly=0;const jy=$('#joy');
|
| 642 |
cv.addEventListener('touchstart',e=>{e.preventDefault();for(const t of e.changedTouches){if(t.clientX<innerWidth*.45&&joyId===null){joyId=t.identifier;joyO=[t.clientX,t.clientY];jy.style.left=t.clientX+'px';jy.style.top=t.clientY+'px';jy.style.display='block';jy.firstChild.style.transform='none'}else if(lookId===null){lookId=t.identifier;lx=t.clientX;ly=t.clientY}}},{passive:false});
|
| 643 |
cv.addEventListener('touchmove',e=>{e.preventDefault();for(const t of e.changedTouches){if(t.identifier===joyId){let dx=(t.clientX-joyO[0])/50,dy=(t.clientY-joyO[1])/50;const l=Math.hypot(dx,dy);if(l>1){dx/=l;dy/=l}joy.x=dx;joy.y=dy;jy.firstChild.style.transform=`translate(${dx*40}px,${dy*40}px)`}else if(t.identifier===lookId){look((t.clientX-lx)*1.5,(t.clientY-ly)*1.5);lx=t.clientX;ly=t.clientY}}},{passive:false});
|
| 644 |
const tend=e=>{for(const t of e.changedTouches){if(t.identifier===joyId){joyId=null;joy.x=joy.y=0;jy.style.display='none'}if(t.identifier===lookId)lookId=null}};cv.addEventListener('touchend',tend);cv.addEventListener('touchcancel',tend);
|
| 645 |
const bind=(id,dn,up)=>{const b=$(id);b.addEventListener('pointerdown',e=>{e.preventDefault();dn()});['pointerup','pointerleave','pointercancel'].forEach(k=>b.addEventListener(k,()=>up&&up()))};
|
| 646 |
+
bind('#bf',()=>{fireHeld=true;doFire()},()=>fireHeld=false);bind('#bj',()=>keys.Space=true,()=>keys.Space=false);bind('#bs',toggleScope);
|
| 647 |
+
$$('.slot').forEach(s=>s.addEventListener('pointerdown',e=>{e.preventDefault();if(playing)setWp(s.dataset.w)}));
|
| 648 |
+
setWp('k');
|
| 649 |
+
|
| 650 |
</script></body></html>'''
|
| 651 |
|
| 652 |
if __name__ == "__main__":
|