Oth1 commited on
Commit
2b61614
·
verified ·
1 Parent(s): fc7229f

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +397 -233
main.py CHANGED
@@ -1,4 +1,4 @@
1
- """Knife & Sniper: Telegram Mini App game. 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,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"], "diff": R["diff"], "train": R["train"],
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"], "bot": bool(p.get("bot"))} for p in R["players"].values()]})
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, "train": R["train"]}
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 and R["state"] == "end": await to_lobby(R)
 
 
 
114
  asyncio.create_task(back())
115
 
116
  async def kill(R, K, V, w):
117
- V["alive"] = False; V["rt"] = time.time() + (1.5 if R["train"] else 3)
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 not R["train"] and R["sc"][K["team"]] >= WIN_KILLS: await finish(R, K["team"])
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
- hum = [p for p in R["players"].values() if not p.get("bot")]
128
- if not hum: rooms.pop(R["code"], None); return
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, diff="normal", train=False, state="lobby", players={}, sc={"H": 0, "V": 0}, boxes=[], go=0, end=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 in ("start", "npc", "train") and host and R["state"] == "lobby":
270
- e = await begin(R, "pvp" if t == "start" else t, now)
271
- if e: await tx(P, {"t": "err", "m": e})
272
- elif t == "diff" and host and R["state"] == "lobby" and d.get("m") in DIFF:
273
- R["diff"] = d["m"]; await rs(R)
274
- elif t == "quit" and host and R["train"] and R["state"] == "play": await to_lobby(R)
 
 
 
275
  elif t == "st" and R["state"] == "play" and P["alive"]:
276
- try: P.update(x=float(d["x"]), y=float(d["y"]), z=float(d["z"]), yaw=float(d["yaw"]), pitch=float(d["pitch"]))
 
 
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": 0 if R["train"] else max(0, int(R["end"] - now)),
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"] and not R["train"]:
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"🔪🎯 تمت دعوتك للغرفة {code}" if code else "🔪🎯 سكاكين وسنايبرات\nاضغط الزر لإنشاء غرفة وادعُ أصدقاءك.", buttons=kb)
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>سكاكين وسنايبرات</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;--ink:#140c2b;--bone:#f4e9d8;--am:#ffb454;--tl:#47e3c8;--rd:#ff7a5c}
353
- *{box-sizing:border-box}html,body{margin:0;height:100%;background:var(--ink);color:var(--bone);font-family:"Segoe UI",Tahoma,"Noto Sans Arabic",system-ui,sans-serif;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none}
354
- canvas{position:fixed;inset:0;width:100%;height:100%}
355
- #lobby{position:fixed;inset:0;display:flex;flex-direction:column;gap:12px;padding:calc(env(safe-area-inset-top,0px) + 14px) 16px 16px;overflow:auto;touch-action:pan-y;background:var(--ink)}
356
- h1{margin:0;font-size:24px;color:var(--am)}button{font:inherit;color:var(--ink);background:var(--am);border:0;border-radius:10px;padding:10px 16px;font-weight:800}
357
- button.alt{background:#2c2150;color:var(--bone)}button.on{background:var(--tl);color:var(--ink)}button:disabled{opacity:.4}
358
- .rw{display:flex;gap:8px;align-items:center;flex-wrap:wrap}#code{font-size:30px;font-weight:900;letter-spacing:4px;direction:ltr;color:var(--tl)}
359
- .row{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#21173f;border-radius:10px}.row b{flex:1}
360
- .av{width:38px;height:38px;border-radius:50%;background:#3b2d68;display:flex;align-items:center;justify-content:center;overflow:hidden;font-weight:800}.av img{width:100%;height:100%;object-fit:cover}
361
- .tm{font-style:normal;padding:3px 10px;border-radius:8px;background:#3b2d68}.tm.H{background:var(--tl);color:var(--ink)}.tm.V{background:var(--rd);color:var(--ink)}
362
- #msg{display:none;padding:10px;background:#4a1f3a;border-radius:10px}
363
- #hud{position:fixed;inset:0;display:none;pointer-events:none}#hud>*{pointer-events:none}
364
- #sc{position:absolute;top:calc(env(safe-area-inset-top,0px) + 8px);left:0;right:0;text-align:center;font-size:20px;font-weight:800;text-shadow:0 1px 6px #000}
365
- #sc .h{color:var(--tl)}#sc .v{color:var(--rd)}
366
- #feed{position:absolute;top:calc(env(safe-area-inset-top,0px) + 44px);right:10px;font-size:13px;text-align:right;text-shadow:0 1px 4px #000}
367
- #cdn,#death,#over{position:absolute;inset:0;display:none;align-items:center;justify-content:center;text-align:center;font-weight:900;text-shadow:0 2px 12px #000}
368
- #cdn{font-size:90px;color:var(--am)}#death{font-size:24px;background:rgba(120,20,30,.35)}#over{font-size:30px;background:rgba(20,12,43,.75);flex-direction:column;gap:8px}
369
- #scope{position:absolute;inset:0;display:none;background:radial-gradient(circle at 50% 50%,transparent 0,transparent 30vmin,#000 30.5vmin)}
370
- #scope:before,#scope:after{content:"";position:absolute;background:#000b}#scope:before{left:50%;top:calc(50% - 30vmin);width:1px;height:60vmin}#scope:after{top:50%;left:calc(50% - 30vmin);height:1px;width:60vmin}
371
- #dot{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:#fff8;box-shadow:0 0 0 1px #0008}
372
- .b{display:none;position:absolute;pointer-events:auto!important;align-items:center;justify-content:center;border-radius:50%;background:#ffffff26;border:2px solid #ffffff66;color:#fff;font-weight:800;touch-action:none}
373
- .touch .b{display:flex}#bf{right:22px;bottom:calc(env(safe-area-inset-bottom,0px) + 28px);width:86px;height:86px;background:#ff7a5c88}#bj{right:122px;bottom:calc(env(safe-area-inset-bottom,0px) + 22px);width:62px;height:62px}
374
- #bw{right:30px;bottom:calc(env(safe-area-inset-bottom,0px) + 128px);width:62px;height:62px}#bs{right:100px;bottom:calc(env(safe-area-inset-bottom,0px) + 100px);width:62px;height:62px}
375
- #rot{position:fixed;inset:0;z-index:99;display:none;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:24px;background:var(--ink);font-size:22px;font-weight:800;text-align:center}
376
- #rot small{font-size:14px;font-weight:400;opacity:.7;direction:ltr}.ph{width:46px;height:78px;border:4px solid var(--am);border-radius:10px;animation:tilt 2.6s ease-in-out infinite}
377
- @keyframes tilt{0%,25%{transform:rotate(0)}65%,100%{transform:rotate(-90deg)}}@media (prefers-reduced-motion:reduce){.ph{animation:none;transform:rotate(-90deg)}}
378
- #bx{display:none;position:absolute;top:calc(env(safe-area-inset-top,0px) + 8px);left:10px;pointer-events:auto!important;padding:6px 14px;background:#2c2150;color:var(--bone)}
379
- #joy{position:absolute;display:none;width:100px;height:100px;margin:-50px;border-radius:50%;border:2px solid #fff6;background:#ffffff14}#joy i{position:absolute;left:50%;top:50%;width:44px;height:44px;margin:-22px;border-radius:50%;background:#ffffff66}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  </style></head><body>
381
  <canvas id="cv"></canvas>
382
- <div id="rot"><div class="ph"></div><div>دوّر جوالك للوضع الأفقي</div><small>Rotate your phone to landscape</small></div>
383
- <div id="lobby"><h1>🔪 سكاكين وسنايبرات 🎯</h1><div id="msg"></div>
384
- <div class="rw"><span>الغرفة</span><span id="code">-----</span><button data-a="share">دعوة</button><button class="alt" data-a="copy">نسخ الرابط</button></div>
385
- <div id="plist" style="display:flex;flex-direction:column;gap:6px"></div>
386
- <div id="teams" class="rw"><span>فريقك</span><button class="alt" data-a="team:H" id="tH">مضيف</button><button class="alt" data-a="team:V" id="tV">زائر</button></div>
387
- <div id="hostctl" style="display:none"><div class="rw" style="margin-bottom:8px"><span>الفرق</span><button class="alt" data-a="mode:manual" id="mm">يدوي</button><button class="alt" data-a="mode:random" id="mr">عشوائي</button></div>
388
- <div class="rw" style="margin-bottom:12px"><span>الخريطة</span><button class="alt" data-a="map:0" id="m0">أطلال الصحراء</button><button class="alt" data-a="map:1" id="m1">القاعدة الثلجية</button><button class="alt" data-a="map:2" id="m2">أسطح المدينة</button></div>
389
- <button data-a="start" style="width:100%;font-size:20px;padding:14px">ابدأ المباراة</button>
390
- <div class="rw" style="margin:14px 0 8px"><span>مستوى NPC</span><button class="alt" data-a="diff:easy" id="d_easy">سهل</button><button class="alt" data-a="diff:normal" id="d_normal">عادي</button><button class="alt" data-a="diff:hard" id="d_hard">صعب</button></div>
391
- <div class="rw"><button class="alt" data-a="npc" style="flex:1">العب ضد NPC</button><button class="alt" data-a="train" style="flex:1">تدريب</button></div></div>
392
- <div id="wait" style="display:none;opacity:.7">بانتظار صاحب الغرفة ليبدأ المباراة...</div></div>
393
- <div id="hud"><button id="bx" data-a="quit">خروج</button><div id="sc"></div><div id="feed"></div><div id="dot"></div><div id="scope"></div><div id="cdn"></div><div id="death"></div><div id="over"></div><div id="joy"><i></i></div>
394
- <button class="b" id="bf">اضرب</button><button class="b" id="bj">قفز</button><button class="b" id="bw">سلاح</button><button class="b" id="bs">منظار</button></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  <script>
396
- const $=s=>document.querySelector(s),esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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
- let train=false,ws,me,Rm,boxes=[],others={},alive=false,playing=false,goAt=0,wp='k',scoped=false,cd=0,myTeam='H',fireHeld=false,mapI=0,keys={},joy={x:0,y:0};
 
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;show('#msg',1)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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='تم قتلك'+(m.by?' بواسطة '+esc(m.by):'')+'<br>ستعود قريباً';show('#death','flex');break;
415
  case'snap':snap(m);break;
416
- case'kill':{const d=document.createElement('div'),c=t=>t==='H'?'#47e3c8':'#ff7a5c';d.innerHTML=(m.k?`<b style="color:${c(m.kt)}">${esc(m.k)}</b> ${m.w==='k'?'🔪':'🎯'} `:'☠️ ')+`<b style="color:${c(m.vt)}">${esc(m.v)}</b>`;$('#feed').prepend(d);setTimeout(()=>d.remove(),4000);break}
417
- case'shot':{const o=new THREE.Vector3(...m.o),dv=new THREE.Vector3(...m.d),g=new THREE.BufferGeometry().setFromPoints([o.clone().addScaledVector(dv,1),o.clone().addScaledVector(dv,90)]);
418
- const l=new THREE.Line(g,new THREE.LineBasicMaterial({color:0xffe9a8}));scene.add(l);setTimeout(()=>scene.remove(l),120);break}
419
- case'end':{alive=false;document.exitPointerLock&&document.exitPointerLock();const w=m.win==='T'?'تعادل':(m.win==='H'?'فاز المضيف':'فاز الزائر');$('#over').innerHTML=`<div>${w}</div><div style="font-size:16px;opacity:.8">${m.win===myTeam?'مبروك فريقك!':m.win==='T'?'':'حظ أوفر'}</div>`;show('#over','flex');break}
 
 
 
420
  }}
421
- function lobby(){show('#lobby','flex');show('#hud',0);const host=Rm.host===me;$('#code').textContent=Rm.code;
422
- $('#plist').innerHTML=Rm.players.map(p=>`<div class="row"><span class="av">${p.photo?`<img src="${esc(p.photo)}">`:esc((p.name||'?')[0])}</span><b>${esc(p.name)}${p.id===Rm.host?' 👑':''}</b><em class="tm ${p.team||''}">${p.team==='H'?'مضيف':p.team==='V'?'زائر':'-'}</em></div>`).join('');
423
- const mine=(Rm.players.find(p=>p.id===me)||{}).team;show('#teams',Rm.mode==='manual'?'flex':0);$('#tH').classList.toggle('on',mine==='H');$('#tV').classList.toggle('on',mine==='V');
424
- 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));['easy','normal','hard'].forEach(k=>$('#d_'+k).classList.toggle('on',Rm.diff===k))}
425
- document.addEventListener('click',e=>{const b=e.target.closest('[data-a]');if(!b)return;const[a,v]=b.dataset.a.split(':');
426
- 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'});else if(a==='npc')send({t:'npc'});else if(a==='train')send({t:'train'});else if(a==='diff')send({t:'diff',m:v});else if(a==='quit')send({t:'quit'});
427
- else if(a==='share'&&Rm){const u='https://t.me/share/url?url='+encodeURIComponent(Rm.link)+'&text='+encodeURIComponent('تعال العب معي! كود الغرفة '+Rm.code);TG?TG.openTelegramLink(u):window.open(u)}
 
 
 
 
 
428
  else if(a==='copy'&&Rm){try{navigator.clipboard.writeText(Rm.link);note('تم نسخ الرابط')}catch(_){note(Rm.link)}}});
429
-
430
- // ---- 3D ----
431
- const renderer=new THREE.WebGLRenderer({canvas:$('#cv'),antialias:false}),scene=new THREE.Scene(),cam=new THREE.PerspectiveCamera(75,1,.1,150);cam.rotation.order='YXZ';scene.add(cam);
432
- scene.add(new THREE.HemisphereLight(0xffffff,0x556677,.9));const sun=new THREE.DirectionalLight(0xffffff,.7);sun.position.set(20,40,10);scene.add(sun);
433
- function fit(){renderer.setPixelRatio(Math.min(devicePixelRatio||1,1.5));renderer.setSize(innerWidth,innerHeight,false);cam.aspect=innerWidth/innerHeight;cam.updateProjectionMatrix()}addEventListener('resize',fit);fit();
434
- const PAL=[{sky:0xf2b880,gr:0xc9a46b,bx:[0xb98b5a,0xa17848,0xd0a56f]},{sky:0xbcd4e6,gr:0xe8eef2,bx:[0x8fa3b5,0x6f8497,0xb3c2cf]},{sky:0x2c2a5a,gr:0x3a3a4a,bx:[0x5a5470,0x474058,0x7a6f8a]}];
435
- let world=new THREE.Group();scene.add(world);
436
- function buildWorld(i){scene.remove(world);world=new THREE.Group();scene.add(world);const c=PAL[i];scene.background=new THREE.Color(c.sky);scene.fog=new THREE.Fog(c.sky,25,90);
437
- const g=new THREE.Mesh(new THREE.PlaneGeometry(80,80),new THREE.MeshLambertMaterial({color:c.gr}));g.rotation.x=-Math.PI/2;world.add(g);
438
- boxes.forEach((b,k)=>{const m=new THREE.Mesh(new THREE.BoxGeometry(b[3],b[4],b[5]),new THREE.MeshLambertMaterial({color:c.bx[k%3]}));m.position.set(b[0],b[1]+b[4]/2,b[2]);world.add(m)})}
439
- const kn=new THREE.Mesh(new THREE.BoxGeometry(.04,.05,.45),new THREE.MeshLambertMaterial({color:0xdddddd})),sn=new THREE.Mesh(new THREE.BoxGeometry(.07,.08,1.2),new THREE.MeshLambertMaterial({color:0x333344}));
440
- kn.position.set(.28,-.28,-.55);kn.rotation.y=.3;sn.position.set(.25,-.25,-.8);cam.add(kn,sn);
441
- function avatar(p){const g=new THREE.Group(),L=(c)=>new THREE.MeshLambertMaterial({color:c});
442
- const a=new THREE.Mesh(new THREE.BoxGeometry(.7,1,.4),L(p.team==='H'?0x47e3c8:0xff7a5c));a.position.y=.95;
443
- const l=new THREE.Mesh(new THREE.BoxGeometry(.6,.5,.35),L(0x222233));l.position.y=.25;const h=new THREE.Mesh(new THREE.SphereGeometry(.24,8,8),L(0xe0b090));h.position.y=1.65;g.add(a,l,h);
444
- 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='#fff';x.strokeStyle='#000';x.lineWidth=5;x.strokeText(p.name,128,42);x.fillText(p.name,128,42);
445
- const s=new THREE.Sprite(new THREE.SpriteMaterial({map:new THREE.CanvasTexture(c),depthTest:false}));s.scale.set(2,.5,1);s.position.y=2.2;g.add(s)}
446
- scene.add(g);return g}
447
- function snap(m){$('#sc').innerHTML=train?`<span class="h">تدريب - القتلى ${m.sc[0]}</span>`:`<span class="h">مضيف ${m.sc[0]}</span> &nbsp; ${Math.floor(m.left/60)}:${String(m.left%60).padStart(2,'0')} &nbsp; <span class="v">${m.sc[1]} زائر</span>`;
448
- for(const[id,x,y,z,yaw,al]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]={g:avatar(pl),t:[x,y,z]};o.g.position.set(x,y,z)}o.t=[x,y,z];o.g.rotation.y=yaw;o.g.visible=!!al}}
449
- function endGame(){show('#bx',0);playing=false;alive=false;scoped=false;fireHeld=false;for(const k in others){scene.remove(others[k].g)}others={};show('#over',0);show('#death',0);show('#scope',0)}
450
- function startGame(m){endGame();boxes=m.boxes;mapI=m.map;myTeam=m.team;train=!!m.train;show('#bx',train?1:0);buildWorld(m.map);show('#lobby',0);show('#hud',1);show('#over',0);$('#feed').innerHTML='';playing=true;goAt=performance.now()+m.cd*1000;wp='k';setWp('k')}
451
- function setWp(w){wp=w;if(w==='k')scoped=false;kn.visible=wp==='k';sn.visible=wp==='s'&&!scoped;show('#scope',scoped?1:0);show('#dot',scoped?0:1);$('#bs').style.visibility=wp==='s'?'visible':'hidden'}
452
- function toggleScope(){if(wp==='s'){scoped=!scoped;setWp('s')}}
453
- function doFire(){const n=performance.now();if(!alive||n<goAt||n<cd)return;cd=n+(wp==='k'?450:1600);send({t:'fire',w:wp});if(wp==='k')kn.rotation.x=-1.2;else{sn.position.z=-.6;P.pitch+=.02}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 R=.4;
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+R-Math.abs(nx-x),oz=d/2+R-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}}}
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+R-.02&&Math.abs(P.z-z)<d/2+R-.02&&P.y>=top-.55&&top>gr)gr=top}
461
- 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}
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){if(n<goAt){$('#cdn').textContent=Math.ceil((goAt-n)/1000);show('#cdn','flex')}else show('#cdn',0);
465
- 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})}if(P.y<-12){send({t:'fall'});alive=false}}
466
- cam.position.set(P.x,P.y+1.6,P.z);cam.rotation.set(P.pitch,P.yaw,0);const tf=scoped?18:75;cam.fov+=(tf-cam.fov)*Math.min(1,dt*14);cam.updateProjectionMatrix();
467
- kn.rotation.x*=.85;sn.position.z+=(-.8-sn.position.z)*Math.min(1,dt*8)}
468
- for(const k in others){const o=others[k],p=o.g.position;p.x+=(o.t[0]-p.x)*.3;p.y+=(o.t[1]-p.y)*.3;p.z+=(o.t[2]-p.z)*.3}
469
- renderer.render(scene,cam)}
 
 
 
 
470
  requestAnimationFrame(frame);
471
- function orient(){const land=innerWidth>innerHeight;show('#rot',touch&&!land?'flex':0);if(touch&&land){try{TG&&TG.lockOrientation&&TG.lockOrientation()}catch(e){}}}
472
- addEventListener('resize',orient);addEventListener('orientationchange',orient);orient();
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('#bw',()=>setWp(wp==='k'?'s':'k'));bind('#bs',toggleScope);
 
 
 
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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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__":