Guest User

djvagodakaravan2d

a guest
Aug 7th, 2025
44
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 23.11 KB | Gaming | 0 0
  1. <!doctype html>
  2. <html lang="ru">
  3. <head>
  4.   <meta charset="utf-8">
  5.   <meta name="viewport" content="width=device-width, initial-scale=1">
  6.   <title>Topdown Realms — 2D с анимацией спрайтов</title>
  7.   <style>
  8.     :root { color-scheme: dark; }
  9.     html, body { height:100%; margin:0; background:#0c0c0f; color:#e9eef3; font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
  10.     #wrap { position:relative; height:100%; display:flex; align-items:center; justify-content:center; }
  11.     #game { width:min(100vw, 1200px); height:min(100vh, 700px); display:block; border:1px solid #1f2a37; border-radius:12px; background:#0f1115; box-shadow: 0 10px 30px rgba(0,0,0,.35); }
  12.     #hud { position:fixed; top:10px; left:12px; right:12px; display:flex; gap:10px; flex-wrap:wrap; z-index:10; }
  13.     .pill { padding:6px 10px; border-radius:999px; background:#17212b; border:1px solid #2a3642; font-size:12px; }
  14.     .danger { color:#ffb4b4; }
  15.     #log { position:fixed; left:12px; right:12px; bottom:12px; height:110px; overflow:auto; font-size:12px; background:rgba(0,0,0,.35); border:1px solid #2a3642; border-radius:10px; padding:8px; z-index:10; }
  16.     #menu, #shop, #help { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; background:rgba(6,8,10,.62); z-index:20; }
  17.     .panel { width:min(92vw, 820px); background:#0f141a; border:1px solid #233140; border-radius:14px; padding:16px 18px; }
  18.     .grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(210px,1fr)); gap:12px; }
  19.     button { background:#2e7d32; color:#fff; border:none; padding:10px 14px; border-radius:10px; cursor:pointer; font-weight:600; }
  20.     button.secondary { background:#273341; }
  21.     a { color:#8ecae6; text-decoration: none; }
  22.     #halfBlind { position:fixed; top:0; right:0; bottom:0; width:50vw; background:#000; opacity:.7; display:none; pointer-events:none; z-index:15; }
  23.   </style>
  24. </head>
  25. <body>
  26.   <div id="wrap"><canvas id="game"></canvas></div>
  27.  
  28.   <div id="hud">
  29.     <span class="pill" id="factionPill">Фракция: —</span>
  30.     <span class="pill">Золото: <b id="gold">0</b></span>
  31.     <span class="pill">HP: <b id="hp">100</b></span>
  32.     <span class="pill">Сост.: <b id="status">OK</b></span>
  33.     <span class="pill">Управление: WASD — ход • ЛКМ/Space — удар • B — лавка • H — помощь</span>
  34.   </div>
  35.   <div id="log"></div>
  36.   <div id="halfBlind"></div>
  37.  
  38.   <div id="menu">
  39.     <div class="panel">
  40.       <h2 style="margin:4px 0 10px">Topdown Realms — 2D сверху со спрайтами</h2>
  41.       <p class="muted">Выберите фракцию и нажмите «Играть». Игра современная (плавные спрайты, high‑dpi), без сервера — просто файл.</p>
  42.       <div class="grid">
  43.         <div>
  44.           <h3>Лесные эльфы</h3>
  45.           <p>Густой лес, налёты, грабеж караванов.</p>
  46.           <button data-faction="elf">Играть за эльфа</button>
  47.         </div>
  48.         <div>
  49.           <h3>Охрана дворца</h3>
  50.           <p>Подчиняйтесь командиру, защищайте дворец.</p>
  51.           <button data-faction="guard">Играть за стражу</button>
  52.         </div>
  53.         <div>
  54.           <h3>Злой повелитель</h3>
  55.           <p>Форт в горах, свои отряды, приказы 1–3.</p>
  56.           <button data-faction="evil">Играть за злого</button>
  57.         </div>
  58.       </div>
  59.       <div style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap">
  60.         <button id="helpBtn" class="secondary">Справка</button>
  61.         <button id="startClose" class="secondary">Закрыть меню</button>
  62.       </div>
  63.     </div>
  64.   </div>
  65.  
  66.   <div id="shop" style="display:none">
  67.     <div class="panel">
  68.       <h3 style="margin:4px 0 10px">Лавка протезов и лечения</h3>
  69.       <div id="shopItems" class="grid"></div>
  70.       <div style="margin-top:8px"><button id="shopClose" class="secondary">Закрыть (B)</button></div>
  71.     </div>
  72.   </div>
  73.  
  74.   <div id="help" style="display:none">
  75.     <div class="panel">
  76.       <h3 style="margin:4px 0 10px">Помощь</h3>
  77.       <ul>
  78.         <li>Карта из 4 зон: 1) Люди/караваны, 2) Дворец, 3) Лес эльфов, 4) Горы/форт Злого.</li>
  79.         <li>Травмы: рука (нет атак), нога (медленно/ползком), глаз (пол‑экрана темно). Лечится/протезы — B.</li>
  80.         <li>Караваны ездят в зоне людей — можно «облегчить» охрану и собрать золото.</li>
  81.         <li>За злого: 1 — атака дворца, 2 — налёт на эльфов, 3 — караваны.</li>
  82.         <li>Сохранение золота и состояния — локально (localStorage).</li>
  83.       </ul>
  84.       <div style="margin-top:8px"><button id="helpClose" class="secondary">Понятно</button></div>
  85.     </div>
  86.   </div>
  87.  
  88. <script>
  89. // ===== High‑DPI Canvas bootstrap =====
  90. const canvas = document.getElementById('game');
  91. const ctx = canvas.getContext('2d');
  92. ctx.imageSmoothingEnabled = true;
  93. function fitCanvas() {
  94.   const dpr = Math.max(1, window.devicePixelRatio || 1);
  95.   const cssW = canvas.clientWidth || 1000;
  96.   const cssH = canvas.clientHeight || 600;
  97.   canvas.width = Math.round(cssW * dpr);
  98.   canvas.height = Math.round(cssH * dpr);
  99.   ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS pixels
  100. }
  101. window.addEventListener('resize', fitCanvas);
  102. fitCanvas();
  103.  
  104. // ===== Utils =====
  105. const rand = (a,b)=>a+Math.random()*(b-a);
  106. const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
  107. const dist2=(a,b)=>{const dx=a.x-b.x,dy=a.y-b.y; return dx*dx+dy*dy;};
  108.  
  109. // ===== Save =====
  110. const Save = {
  111.   key: 'topdown-realms-v1',
  112.   load(){ try{ const r=localStorage.getItem(this.key); return r?JSON.parse(r):null; }catch(e){ return null; } },
  113.   save(s){ try{ const prev=this.load()||{}; localStorage.setItem(this.key, JSON.stringify({...prev, ...s})); }catch(e){} }
  114. };
  115.  
  116. // ===== UI =====
  117. const ui = {
  118.   factionEl: document.getElementById('factionPill'),
  119.   goldEl: document.getElementById('gold'),
  120.   hpEl: document.getElementById('hp'),
  121.   statusEl: document.getElementById('status'),
  122.   logEl: document.getElementById('log'),
  123.   halfBlind: document.getElementById('halfBlind'),
  124.   shopEl: document.getElementById('shop'),
  125.   shopItemsEl: document.getElementById('shopItems'),
  126.   setFaction(f){ this.factionEl.textContent='Фракция: '+({elf:'Эльф',guard:'Страж',evil:'Злой'}[f]||f); },
  127.   log(t){ const p=document.createElement('div'); p.textContent='• '+t; this.logEl.appendChild(p); this.logEl.scrollTop=this.logEl.scrollHeight; },
  128.   updateHUD(p){
  129.     this.goldEl.textContent = game.gold|0;
  130.     this.hpEl.textContent = Math.max(0, Math.round(p.hp));
  131.     const st=[]; if(p.inj.arm) st.push('нет правой руки'); if(p.inj.leg2) st.push('оба колена'); else if(p.inj.leg1) st.push('травма ноги'); if(p.inj.eye) st.push('полслепой');
  132.     this.statusEl.innerHTML = st.length ? '<span class="danger">'+st.join(', ')+'</span>' : 'OK';
  133.     this.halfBlind.style.display = p.inj.eye ? 'block' : 'none';
  134.   },
  135.   renderShop(){
  136.     const items=[
  137.       {id:'heal', title:'Лечение (+40 HP)', cost:10},
  138.       {id:'arm', title:'Протез руки', cost:18},
  139.       {id:'leg', title:'Протез ноги (комплект)', cost:24},
  140.       {id:'eye', title:'Протез глаза', cost:15},
  141.     ];
  142.     this.shopItemsEl.innerHTML='';
  143.     for(const it of items){
  144.       const d=document.createElement('div');
  145.       d.innerHTML='<b>'+it.title+'</b><div style="color:#9fb1c1;font-size:12px">Исправляет соответствующую травму</div>';
  146.       const b=document.createElement('button'); b.textContent='Купить — '+it.cost+' зол.';
  147.       b.onclick=()=>game.buy(it);
  148.       d.appendChild(b); this.shopItemsEl.appendChild(d);
  149.     }
  150.   },
  151.   toggleShop(force){ const el=this.shopEl; if(typeof force==='boolean') el.style.display=force?'flex':'none'; else el.style.display=(el.style.display==='flex'?'none':'flex'); }
  152. };
  153. ui.renderShop();
  154.  
  155. // ===== Input =====
  156. const keys = {};
  157. let attackPressed=false;
  158. window.addEventListener('keydown', (e)=>{ keys[e.code]=true;
  159.   if(e.code==='KeyB') ui.toggleShop();
  160.   if(e.code==='KeyH') document.getElementById('help').style.display='flex';
  161. });
  162. window.addEventListener('keyup', (e)=>{ keys[e.code]=false; });
  163. canvas.addEventListener('mousedown', ()=>{ attackPressed=true; });
  164.  
  165. // ===== Sprite Atlases (runtime-generated, smooth) =====
  166. function makeHumanoidAtlas(color) {
  167.   const frameW=64, frameH=64, cols=3, rows=4;
  168.   const oc=document.createElement('canvas'); oc.width=frameW*cols; oc.height=frameH*rows;
  169.   const g=oc.getContext('2d');
  170.   g.lineJoin='round'; g.lineCap='round';
  171.   function drawOne(x,y,dir,frame){
  172.     g.save(); g.translate(x+frameW/2, y+frameH/2);
  173.     // shadow
  174.     g.fillStyle='rgba(0,0,0,0.25)'; g.beginPath(); g.ellipse(0,18,14,6,0,0,Math.PI*2); g.fill();
  175.     // walking phase
  176.     const t = frame===0?0 : frame===1?0.5 : 1.0;
  177.     const legSwing = Math.sin(t*Math.PI*2)*8;
  178.     const armSwing = -legSwing;
  179.     // Direction: 0=down,1=left,2=right,3=up
  180.     let facing = dir;
  181.     // body
  182.     g.fillStyle=color; g.strokeStyle='#0a0f14'; g.lineWidth=1.5;
  183.     // torso
  184.     g.beginPath(); g.moveTo(-10,-6); g.quadraticCurveTo(0,-10,10,-6); g.lineTo(10,10); g.quadraticCurveTo(0,14,-10,10); g.closePath(); g.fill(); g.stroke();
  185.     // head
  186.     g.fillStyle='#f1d7b7'; g.beginPath(); g.arc(0,-16,9,0,Math.PI*2); g.fill(); g.stroke();
  187.     // eye(s)
  188.     g.fillStyle='#17212b'; if(facing===0||facing===1||facing===2){ g.fillRect(-4,-18,2,2); g.fillRect(2,-18,2,2); } else { g.fillRect(-2,-18,4,2); }
  189.     // arms
  190.     g.strokeStyle='#e8e8e8'; g.lineWidth=4;
  191.     g.beginPath(); g.moveTo(-10,-4); g.lineTo(-10,6 + Math.sin(t*6)*3); g.stroke();
  192.     g.beginPath(); g.moveTo(10,-4);  g.lineTo(10, 6 + Math.sin(t*6+Math.PI)*3); g.stroke();
  193.     // legs
  194.     g.strokeStyle='#222'; g.lineWidth=5;
  195.     g.beginPath(); g.moveTo(-6,10); g.lineTo(-6, 18 + Math.sin(t*6)*3); g.stroke();
  196.     g.beginPath(); g.moveTo(6,10);  g.lineTo(6, 18 + Math.sin(t*6+Math.PI)*3); g.stroke();
  197.     g.restore();
  198.   }
  199.   for(let r=0;r<rows;r++){
  200.    for(let c=0;c<cols;c++){
  201.      drawOne(c*frameW, r*frameH, r, c);
  202.    }
  203.  }
  204.  const img=new Image(); img.src=oc.toDataURL();
  205.  return {img, frameW, frameH, cols, rows};
  206. }
  207. const atlases = {
  208.  elf:   makeHumanoidAtlas('#2ea44f'),
  209.  guard: makeHumanoidAtlas('#2d6cdf'),
  210.  evil:  makeHumanoidAtlas('#d14b4b'),
  211. };
  212.  
  213. // ===== World / Map (static layer to offscreen) =====
  214. const world = {
  215.  w: 2000, h: 1400, // world pixels
  216.  bg: null,
  217.  zones: {
  218.    HUMANS: {x:0, y:0, w:1000, h:700},
  219.    PALACE: {x:1000, y:0, w:1000, h:700},
  220.    ELVES:  {x:0, y:700, w:1000, h:700},
  221.    EVIL:   {x:1000, y:700, w:1000, h:700},
  222.  },
  223.  obstacles: [], // rectangles for collisions
  224. };
  225. function buildWorld() {
  226.  const oc=document.createElement('canvas'); oc.width=world.w; oc.height=world.h; const g=oc.getContext('2d');
  227.  // zones tint
  228.  g.fillStyle='#16243a'; g.fillRect(0,0,1000,700);
  229.  g.fillStyle='#1b1b1f'; g.fillRect(1000,0,1000,700);
  230.  g.fillStyle='#11281a'; g.fillRect(0,700,1000,700);
  231.  g.fillStyle='#2b1719'; g.fillRect(1000,700,1000,700);
  232.  // roads / paths
  233.  g.fillStyle='#6b4e2e';
  234.  g.fillRect(120, 120, 700, 18); // horizontal
  235.  g.fillRect(1000-18, 120, 18, 460); // up-down to palace
  236.  // town
  237.  g.fillStyle='#d1a170'; roundRect(g, 120-30, 120-24, 60, 48, 6, true);
  238.  // palace
  239.  g.fillStyle='#cfcfcf'; roundRect(g, 1500-60, 350-40, 120, 80, 10, true);
  240.  // elf huts
  241.  for(let i=0;i<5;i++){ g.fillStyle='#8b5a2b'; roundRect(g, 250 + i*70, 1050 + (i%2)*40, 44, 34, 6, true); }
  242.  // forest dots
  243.  for(let i=0;i<600;i++){ const x=rand(80, 900), y=rand(760, 1320); g.fillStyle=Math.random()<0.5?'#1a7b3a':'#146b31'; g.beginPath(); g.arc(x,y, rand(6,14), 0, Math.PI*2); g.fill(); }
  244.  // fort
  245.  g.fillStyle='#666'; roundRect(g, 1500-40, 1100-32, 80, 64, 10, true);
  246.  // mountains
  247.  for(let i=0;i<400;i++){ const x=rand(1080, 1920), y=rand(780, 1320); g.fillStyle='#4b3b40'; g.beginPath(); g.arc(x,y, rand(4,10), 0, Math.PI*2); g.fill(); }
  248.  // labels
  249.  g.fillStyle='#9fb1c1'; g.font='16px system-ui';
  250.  g.fillText('1: Люди / караваны', 20, 30);
  251.  g.fillText('2: Дворец', 1020, 30);
  252.  g.fillText('3: Лес эльфов', 20, 730+30);
  253.  g.fillText('4: Форт Злого', 1020, 730+30);
  254.  world.bg = oc;
  255.  
  256.  // obstacles rectangles (roads not blocking). We'll block major buildings.
  257.  world.obstacles = [
  258.    {x: 1500-60, y:350-40, w:120, h:80}, // palace
  259.    {x: 120-30, y:120-24, w:60, h:48},   // town hall
  260.    {x: 1500-40, y:1100-32, w:80, h:64}, // fort
  261.  ];
  262. }
  263. function roundRect(g, x,y,w,h,r, fill=true, stroke=false){ g.beginPath(); g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); if(fill) g.fill(); if(stroke) g.stroke(); }
  264. buildWorld();
  265. // ===== Entities =====
  266. const F = {ELF:'elf', GUARD:'guard', EVIL:'evil'};
  267. function makeUnit(f,x,y,isPlayer=false){
  268.  return {
  269.    id: Math.random(), f, x, y, vx:0, vy:0, dir:0, // 0 down,1 left,2 right,3 up
  270.    frame:0, frameTimer:0,
  271.    isPlayer, hp:100, dmg:24, range:28,
  272.    inj:{ arm:false, leg1:false, leg2:false, eye:false },
  273.    ai:{ t:0, dir:{x:0,y:0}, order:null, target:null }
  274.  };
  275. }
  276. const game = {
  277.  running:false, faction:null,
  278.  gold:0,
  279.  ents:[], caravans:[], loot:[],
  280.  player:null,
  281.  cam:{x:0,y:0,w:1000,h:600},
  282.  start(f){
  283.    this.running=true; this.faction=f; ui.setFaction(f);
  284.    this.ents=[]; this.caravans=[]; this.loot=[];
  285.    // spawn player
  286.    const sp = f===F.ELF? {x:400,y:1100} : f===F.GUARD? {x:1500,y:300} : {x:1500,y:1100};
  287.    this.player = makeUnit(f, sp.x, sp.y, true); this.ents.push(this.player);
  288.    // load save
  289.    const sv = Save.load(); if(sv){ this.gold=sv.gold||0; Object.assign(this.player.inj, sv.inj||{}); }
  290.    // squads
  291.    for(let i=0;i<6;i++) this.ents.push(makeUnit(F.GUARD, 1400 + (i%3)*30, 260 + Math.floor(i/3)*30));
  292.    for(let i=0;i<6;i++) this.ents.push(makeUnit(F.ELF, 300 + (i%3)*30, 1000 + Math.floor(i/3)*30));
  293.    for(let i=0;i<6;i++) this.ents.push(makeUnit(F.EVIL, 1420 + (i%3)*30, 1060 + Math.floor(i/3)*30));
  294.    // caravans
  295.    spawnCaravans(this);
  296.    ui.log('Начало игры за: '+({elf:'Эльф',guard:'Страж',evil:'Злой'}[f]||f));
  297.  },
  298.  buy(it){
  299.    const p=this.player; if(this.gold < it.cost){ ui.log('Недостаточно золота'); return; }
  300.    if(it.id==='heal'){ p.hp=clamp(p.hp+40,0,100); }
  301.    else if(it.id==='arm' && p.inj.arm){ p.inj.arm=false; }
  302.    else if(it.id==='leg'){ p.inj.leg1=false; p.inj.leg2=false; }
  303.    else if(it.id==='eye' && p.inj.eye){ p.inj.eye=false; }
  304.    this.gold -= it.cost; Save.save({gold:this.gold, inj:p.inj}); ui.log('Куплено: '+it.title+' (-'+it.cost+' зол.)');
  305.  }
  306. };
  307. // ===== Caravans =====
  308. function spawnCaravans(g){
  309.  const path = [{x:180,y:120},{x:820,y:120},{x:820,y:320},{x:820,y:120}];
  310.  for(let k=0;k<2;k++){
  311.    const cart={x:path[0].x, y:path[0].y, seg:0, t:0, speed:90+Math.random()*30, guards:[]};
  312.    const g1=makeUnit(F.GUARD, cart.x-20, cart.y); const g2=makeUnit(F.GUARD, cart.x+20, cart.y);
  313.    cart.guards=[g1,g2]; g.ents.push(g1,g2); g.caravans.push(cart);
  314.  }
  315.  function stepCart(c, dt){
  316.    const a=path[c.seg], b=path[(c.seg+1)%path.length];
  317.    const dx=b.x-a.x, dy=b.y-a.y, dist=Math.hypot(dx,dy);
  318.    c.t += (c.speed*dt)/dist; if(c.t>=1){ c.t=0; c.seg=(c.seg+1)%path.length; if(c.seg===0){ dropGold(c.x,c.y, 4+((Math.random()*6)|0)); } }
  319.     c.x = a.x + dx*c.t; c.y = a.y + dy*c.t;
  320.     if(c.guards){ c.guards[0].x=c.x-20; c.guards[0].y=c.y; c.guards[1].x=c.x+20; c.guards[1].y=c.y; }
  321.   }
  322.   g.stepCaravans = (dt)=>{ for(const c of g.caravans) stepCart(c, dt); };
  323. }
  324. function dropGold(x,y,v){ game.loot.push({x,y,v}); }
  325.  
  326. // ===== Update & Render =====
  327. let last=performance.now();
  328. function loop(){
  329.   const now=performance.now(), dt=Math.min(0.033,(now-last)/1000); last=now;
  330.   if(game.running){ update(dt); }
  331.   draw();
  332.   requestAnimationFrame(loop);
  333. }
  334. requestAnimationFrame(loop);
  335.  
  336. function handleInput(dt){
  337.   const p=game.player; if(!p) return;
  338.   let mx=(keys['KeyD']||keys['ArrowRight']?1:0) - (keys['KeyA']||keys['ArrowLeft']?1:0);
  339.   let my=(keys['KeyS']||keys['ArrowDown']?1:0) - (keys['KeyW']||keys['ArrowUp']?1:0);
  340.   const len=Math.hypot(mx,my); if(len>0){ mx/=len; my/=len; }
  341.   let speed = p.inj.leg2? 40 : p.inj.leg1? 80 : 140;
  342.   p.vx=mx*speed; p.vy=my*speed;
  343.   if(Math.abs(mx)>0.01 || Math.abs(my)>0.01){
  344.     p.dir = Math.abs(mx) > Math.abs(my) ? (mx>0?2:1) : (my>0?0:3);
  345.     p.frameTimer += dt*10; if(p.frameTimer>=1){ p.frame=(p.frame+1)%3; p.frameTimer=0; }
  346.   } else {
  347.     p.frame=1; // idle middle frame
  348.   }
  349.   // attack
  350.   if(keys['Space'] || attackPressed){ attackPressed=false; if(!p.inj.arm) doAttack(p); }
  351. }
  352. function doAttack(src){
  353.   let best=null, dmin=99999;
  354.   for(const e of game.ents){
  355.     if(e===src || e.hp<=0 || e.f===src.f) continue;
  356.    const d=Math.hypot(e.x-src.x, e.y-src.y); if(d<dmin){ dmin=d; best=e; }
  357.  }
  358.  if(best && dmin < src.range){
  359.    best.hp -= src.dmg;
  360.    const r=Math.random();
  361.    if(r<0.12 && best.hp>0){ if(best.inj.leg1) best.inj.leg2=true; else best.inj.leg1=true; if(best.isPlayer) ui.log('Тяжёлая травма ноги!'); }
  362.     else if(r<0.22 && best.hp>0){ best.inj.arm=true; if(best.isPlayer) ui.log('Потеряна правая рука!'); }
  363.     else if(r<0.30 && best.hp>0){ best.inj.eye=true; if(best.isPlayer) ui.log('Выколот глаз!'); }
  364.     if(best.hp<=0){ dropGold(best.x, best.y, 5+((Math.random()*8)|0)); }
  365.  }
  366. }
  367. function tickAI(e, dt){
  368.  // find closest enemy
  369.  let target=null, d2min=999999;
  370.  for(const t of game.ents){ if(t===e||t.hp<=0||t.f===e.f) continue; const d=dist2(t,e); if(d<d2min){ d2min=d; target=t; } }
  371.  let speed = e.inj.leg2? 35 : e.inj.leg1? 65 : 120;
  372.  if(target && d2min < 220*220){
  373.    const dx=target.x-e.x, dy=target.y-e.y, L=Math.hypot(dx,dy)||1;
  374.    e.vx=(dx/L)*speed; e.vy=(dy/L)*speed;
  375.    e.dir = Math.abs(dx) > Math.abs(dy) ? (dx>0?2:1) : (dy>0?0:3);
  376.     e.frameTimer+=dt*10; if(e.frameTimer>=1){ e.frame=(e.frame+1)%3; e.frameTimer=0; }
  377.     if(d2min < 26*26 && !e.inj.arm){ doAttack(e); }
  378.  } else if(e.ai.order && e.ai.order.type==='attack'){
  379.    const dx=e.ai.order.point.x - e.x, dy=e.ai.order.point.y - e.y, L=Math.hypot(dx,dy)||1;
  380.    e.vx=(dx/L)*speed; e.vy=(dy/L)*speed;
  381.    e.dir = Math.abs(dx) > Math.abs(dy) ? (dx>0?2:1) : (dy>0?0:3);
  382.     e.frameTimer+=dt*8; if(e.frameTimer>=1){ e.frame=(e.frame+1)%3; e.frameTimer=0; }
  383.   } else {
  384.     // wander
  385.     e.ai.t-=dt; if(e.ai.t<=0){ e.ai.t=1+rand(0,2); e.ai.dir={x:rand(-1,1), y:rand(-1,1)}; }
  386.    e.vx=e.ai.dir.x*speed*0.5; e.vy=e.ai.dir.y*speed*0.5;
  387.    e.dir = Math.abs(e.vx) > Math.abs(e.vy) ? (e.vx>0?2:1) : (e.vy>0?0:3);
  388.     e.frameTimer+=dt*6; if(e.frameTimer>=1){ e.frame=(e.frame+1)%3; e.frameTimer=0; }
  389.   }
  390. }
  391. function collideRect(e, r){
  392.   const half=12;
  393.   const ex=e.x-half, ey=e.y-half, ew=half*2, eh=half*2;
  394.   return ex < r.x+r.w && ex+ew > r.x && ey < r.y+r.h && ey+eh > r.y;
  395. }
  396. function integrate(e, dt){
  397.   const nx = e.x + e.vx*dt, ny = e.y + e.vy*dt;
  398.   // collisions
  399.   let blocked=false;
  400.   for(const r of world.obstacles){
  401.     const test={x:nx,y:ny}; if(collideRect({x:nx,y:e.y}, r)) { e.vx=0; blocked=true; }
  402.     if(collideRect({x:e.x,y:ny}, r)) { e.vy=0; blocked=true; }
  403.   }
  404.   e.x = clamp(e.x + e.vx*dt, 10, world.w-10);
  405.   e.y = clamp(e.y + e.vy*dt, 10, world.h-10);
  406. }
  407. function update(dt){
  408.   handleInput(dt);
  409.   // AI + move
  410.   for(const e of game.ents){ if(e.isPlayer || e.hp<=0) continue; tickAI(e, dt); }
  411.  for(const e of game.ents){ integrate(e, dt); }
  412.  // caravans
  413.  if(game.stepCaravans) game.stepCaravans(dt);
  414.  // loot pickup
  415.  for(const l of game.loot){ if(l.dead) continue; const d2=dist2(game.player,l); if(d2<26*26){ game.gold+=l.v; l.dead=true; ui.log('+'+l.v+' золота'); Save.save({gold:game.gold, inj:game.player.inj}); } }
  416.  game.loot = game.loot.filter(l=>!l.dead);
  417.   // camera follow
  418.   const cam = game.cam; const p=game.player;
  419.   cam.w = canvas.width / (window.devicePixelRatio||1); cam.h = canvas.height / (window.devicePixelRatio||1);
  420.   cam.x = clamp(p.x - cam.w/2, 0, world.w - cam.w); cam.y = clamp(p.y - cam.h/2, 0, world.h - cam.h);
  421.   // EVIL commands
  422.   if(game.faction===F.EVIL){
  423.     if(keys['Digit1']){ orderAll(F.EVIL, {type:'attack', point:{x:1500,y:350}}); ui.log('Отрядам: штурм дворца!'); }
  424.     if(keys['Digit2']){ orderAll(F.EVIL, {type:'attack', point:{x:400,y:1100}}); ui.log('Отрядам: удар по эльфам!'); }
  425.     if(keys['Digit3']){ orderAll(F.EVIL, {type:'attack', point:{x:820,y:120}}); ui.log('Отрядам: на караваны!'); }
  426.   }
  427.   ui.updateHUD(game.player);
  428. }
  429. function orderAll(f, order){ for(const e of game.ents){ if(!e.isPlayer && e.f===f) e.ai.order=order; } }
  430.  
  431. function draw(){
  432.   const cam=game.cam;
  433.   // bg
  434.   ctx.clearRect(0,0,canvas.width,canvas.height);
  435.   // blit world background
  436.   if(world.bg) ctx.drawImage(world.bg, cam.x, cam.y, cam.w, cam.h, 0,0, cam.w, cam.h);
  437.   // loot
  438.   for(const l of game.loot){ drawCoin(l.x - cam.x, l.y - cam.y); }
  439.   // entities
  440.   for(const e of game.ents){
  441.     const x=e.x - cam.x, y=e.y - cam.y;
  442.     if(e.hp<=0){ // corpse
  443.      ctx.save(); ctx.translate(x,y); ctx.fillStyle='rgba(90,60,40,.85)'; ctx.beginPath(); ctx.ellipse(0,8,14,6,0,0,Math.PI*2); ctx.fill(); ctx.restore(); continue;
  444.    }
  445.    drawDudeSprite(e, x, y);
  446.    // tiny hp bar
  447.    ctx.fillStyle='rgba(0,0,0,.5)'; ctx.fillRect(x-16, y-22, 32, 4);
  448.    ctx.fillStyle='#7CFC7C'; ctx.fillRect(x-16, y-22, 32*(e.hp/100), 4);
  449.  }
  450. }
  451. function drawCoin(x,y){
  452.  ctx.save(); ctx.translate(x,y);
  453.  ctx.fillStyle='#ffd54f'; ctx.beginPath(); ctx.arc(0,0,6,0,Math.PI*2); ctx.fill();
  454.  ctx.strokeStyle='#a67c00'; ctx.lineWidth=1; ctx.stroke();
  455.  ctx.restore();
  456. }
  457. function drawDudeSprite(e, x, y){
  458.  const atlas = atlases[e.f];
  459.  const fw=atlas.frameW, fh=atlas.frameH;
  460.  const sx = e.frame * fw, sy = e.dir * fh;
  461.  ctx.drawImage(atlas.img, sx, sy, fw, fh, x-24, y-36, 48, 72);
  462. }
  463.  
  464. // ===== Menu / overlays =====
  465. document.querySelectorAll('#menu button[data-faction]').forEach(b=>{
  466.   b.addEventListener('click', ()=>{
  467.     document.getElementById('menu').style.display='none';
  468.     const f=b.getAttribute('data-faction'); game.start(f);
  469.   });
  470. });
  471. document.getElementById('startClose').addEventListener('click', ()=>{ document.getElementById('menu').style.display='none'; });
  472. document.getElementById('helpBtn').addEventListener('click', ()=>{ document.getElementById('help').style.display='flex'; });
  473. document.getElementById('helpClose').addEventListener('click', ()=>{ document.getElementById('help').style.display='none'; });
  474. document.getElementById('shopClose').addEventListener('click', ()=>{ ui.toggleShop(false); });
  475.  
  476. // ===== Init log =====
  477. ui.log('WASD — движение • ЛКМ/Space — удар • B — лавка • H — помощь');
  478.  
  479. </script>
  480. </body>
  481. </html>
  482.  
Advertisement
Add Comment
Please, Sign In to add comment