Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!doctype html>
- <html lang="ru">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Topdown Realms — 2D с анимацией спрайтов</title>
- <style>
- :root { color-scheme: dark; }
- html, body { height:100%; margin:0; background:#0c0c0f; color:#e9eef3; font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
- #wrap { position:relative; height:100%; display:flex; align-items:center; justify-content:center; }
- #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); }
- #hud { position:fixed; top:10px; left:12px; right:12px; display:flex; gap:10px; flex-wrap:wrap; z-index:10; }
- .pill { padding:6px 10px; border-radius:999px; background:#17212b; border:1px solid #2a3642; font-size:12px; }
- .danger { color:#ffb4b4; }
- #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; }
- #menu, #shop, #help { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; background:rgba(6,8,10,.62); z-index:20; }
- .panel { width:min(92vw, 820px); background:#0f141a; border:1px solid #233140; border-radius:14px; padding:16px 18px; }
- .grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(210px,1fr)); gap:12px; }
- button { background:#2e7d32; color:#fff; border:none; padding:10px 14px; border-radius:10px; cursor:pointer; font-weight:600; }
- button.secondary { background:#273341; }
- a { color:#8ecae6; text-decoration: none; }
- #halfBlind { position:fixed; top:0; right:0; bottom:0; width:50vw; background:#000; opacity:.7; display:none; pointer-events:none; z-index:15; }
- </style>
- </head>
- <body>
- <div id="wrap"><canvas id="game"></canvas></div>
- <div id="hud">
- <span class="pill" id="factionPill">Фракция: —</span>
- <span class="pill">Золото: <b id="gold">0</b></span>
- <span class="pill">HP: <b id="hp">100</b></span>
- <span class="pill">Сост.: <b id="status">OK</b></span>
- <span class="pill">Управление: WASD — ход • ЛКМ/Space — удар • B — лавка • H — помощь</span>
- </div>
- <div id="log"></div>
- <div id="halfBlind"></div>
- <div id="menu">
- <div class="panel">
- <h2 style="margin:4px 0 10px">Topdown Realms — 2D сверху со спрайтами</h2>
- <p class="muted">Выберите фракцию и нажмите «Играть». Игра современная (плавные спрайты, high‑dpi), без сервера — просто файл.</p>
- <div class="grid">
- <div>
- <h3>Лесные эльфы</h3>
- <p>Густой лес, налёты, грабеж караванов.</p>
- <button data-faction="elf">Играть за эльфа</button>
- </div>
- <div>
- <h3>Охрана дворца</h3>
- <p>Подчиняйтесь командиру, защищайте дворец.</p>
- <button data-faction="guard">Играть за стражу</button>
- </div>
- <div>
- <h3>Злой повелитель</h3>
- <p>Форт в горах, свои отряды, приказы 1–3.</p>
- <button data-faction="evil">Играть за злого</button>
- </div>
- </div>
- <div style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap">
- <button id="helpBtn" class="secondary">Справка</button>
- <button id="startClose" class="secondary">Закрыть меню</button>
- </div>
- </div>
- </div>
- <div id="shop" style="display:none">
- <div class="panel">
- <h3 style="margin:4px 0 10px">Лавка протезов и лечения</h3>
- <div id="shopItems" class="grid"></div>
- <div style="margin-top:8px"><button id="shopClose" class="secondary">Закрыть (B)</button></div>
- </div>
- </div>
- <div id="help" style="display:none">
- <div class="panel">
- <h3 style="margin:4px 0 10px">Помощь</h3>
- <ul>
- <li>Карта из 4 зон: 1) Люди/караваны, 2) Дворец, 3) Лес эльфов, 4) Горы/форт Злого.</li>
- <li>Травмы: рука (нет атак), нога (медленно/ползком), глаз (пол‑экрана темно). Лечится/протезы — B.</li>
- <li>Караваны ездят в зоне людей — можно «облегчить» охрану и собрать золото.</li>
- <li>За злого: 1 — атака дворца, 2 — налёт на эльфов, 3 — караваны.</li>
- <li>Сохранение золота и состояния — локально (localStorage).</li>
- </ul>
- <div style="margin-top:8px"><button id="helpClose" class="secondary">Понятно</button></div>
- </div>
- </div>
- <script>
- // ===== High‑DPI Canvas bootstrap =====
- const canvas = document.getElementById('game');
- const ctx = canvas.getContext('2d');
- ctx.imageSmoothingEnabled = true;
- function fitCanvas() {
- const dpr = Math.max(1, window.devicePixelRatio || 1);
- const cssW = canvas.clientWidth || 1000;
- const cssH = canvas.clientHeight || 600;
- canvas.width = Math.round(cssW * dpr);
- canvas.height = Math.round(cssH * dpr);
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS pixels
- }
- window.addEventListener('resize', fitCanvas);
- fitCanvas();
- // ===== Utils =====
- const rand = (a,b)=>a+Math.random()*(b-a);
- const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
- const dist2=(a,b)=>{const dx=a.x-b.x,dy=a.y-b.y; return dx*dx+dy*dy;};
- // ===== Save =====
- const Save = {
- key: 'topdown-realms-v1',
- load(){ try{ const r=localStorage.getItem(this.key); return r?JSON.parse(r):null; }catch(e){ return null; } },
- save(s){ try{ const prev=this.load()||{}; localStorage.setItem(this.key, JSON.stringify({...prev, ...s})); }catch(e){} }
- };
- // ===== UI =====
- const ui = {
- factionEl: document.getElementById('factionPill'),
- goldEl: document.getElementById('gold'),
- hpEl: document.getElementById('hp'),
- statusEl: document.getElementById('status'),
- logEl: document.getElementById('log'),
- halfBlind: document.getElementById('halfBlind'),
- shopEl: document.getElementById('shop'),
- shopItemsEl: document.getElementById('shopItems'),
- setFaction(f){ this.factionEl.textContent='Фракция: '+({elf:'Эльф',guard:'Страж',evil:'Злой'}[f]||f); },
- log(t){ const p=document.createElement('div'); p.textContent='• '+t; this.logEl.appendChild(p); this.logEl.scrollTop=this.logEl.scrollHeight; },
- updateHUD(p){
- this.goldEl.textContent = game.gold|0;
- this.hpEl.textContent = Math.max(0, Math.round(p.hp));
- 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('полслепой');
- this.statusEl.innerHTML = st.length ? '<span class="danger">'+st.join(', ')+'</span>' : 'OK';
- this.halfBlind.style.display = p.inj.eye ? 'block' : 'none';
- },
- renderShop(){
- const items=[
- {id:'heal', title:'Лечение (+40 HP)', cost:10},
- {id:'arm', title:'Протез руки', cost:18},
- {id:'leg', title:'Протез ноги (комплект)', cost:24},
- {id:'eye', title:'Протез глаза', cost:15},
- ];
- this.shopItemsEl.innerHTML='';
- for(const it of items){
- const d=document.createElement('div');
- d.innerHTML='<b>'+it.title+'</b><div style="color:#9fb1c1;font-size:12px">Исправляет соответствующую травму</div>';
- const b=document.createElement('button'); b.textContent='Купить — '+it.cost+' зол.';
- b.onclick=()=>game.buy(it);
- d.appendChild(b); this.shopItemsEl.appendChild(d);
- }
- },
- 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'); }
- };
- ui.renderShop();
- // ===== Input =====
- const keys = {};
- let attackPressed=false;
- window.addEventListener('keydown', (e)=>{ keys[e.code]=true;
- if(e.code==='KeyB') ui.toggleShop();
- if(e.code==='KeyH') document.getElementById('help').style.display='flex';
- });
- window.addEventListener('keyup', (e)=>{ keys[e.code]=false; });
- canvas.addEventListener('mousedown', ()=>{ attackPressed=true; });
- // ===== Sprite Atlases (runtime-generated, smooth) =====
- function makeHumanoidAtlas(color) {
- const frameW=64, frameH=64, cols=3, rows=4;
- const oc=document.createElement('canvas'); oc.width=frameW*cols; oc.height=frameH*rows;
- const g=oc.getContext('2d');
- g.lineJoin='round'; g.lineCap='round';
- function drawOne(x,y,dir,frame){
- g.save(); g.translate(x+frameW/2, y+frameH/2);
- // shadow
- g.fillStyle='rgba(0,0,0,0.25)'; g.beginPath(); g.ellipse(0,18,14,6,0,0,Math.PI*2); g.fill();
- // walking phase
- const t = frame===0?0 : frame===1?0.5 : 1.0;
- const legSwing = Math.sin(t*Math.PI*2)*8;
- const armSwing = -legSwing;
- // Direction: 0=down,1=left,2=right,3=up
- let facing = dir;
- // body
- g.fillStyle=color; g.strokeStyle='#0a0f14'; g.lineWidth=1.5;
- // torso
- 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();
- // head
- g.fillStyle='#f1d7b7'; g.beginPath(); g.arc(0,-16,9,0,Math.PI*2); g.fill(); g.stroke();
- // eye(s)
- 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); }
- // arms
- g.strokeStyle='#e8e8e8'; g.lineWidth=4;
- g.beginPath(); g.moveTo(-10,-4); g.lineTo(-10,6 + Math.sin(t*6)*3); g.stroke();
- g.beginPath(); g.moveTo(10,-4); g.lineTo(10, 6 + Math.sin(t*6+Math.PI)*3); g.stroke();
- // legs
- g.strokeStyle='#222'; g.lineWidth=5;
- g.beginPath(); g.moveTo(-6,10); g.lineTo(-6, 18 + Math.sin(t*6)*3); g.stroke();
- g.beginPath(); g.moveTo(6,10); g.lineTo(6, 18 + Math.sin(t*6+Math.PI)*3); g.stroke();
- g.restore();
- }
- for(let r=0;r<rows;r++){
- for(let c=0;c<cols;c++){
- drawOne(c*frameW, r*frameH, r, c);
- }
- }
- const img=new Image(); img.src=oc.toDataURL();
- return {img, frameW, frameH, cols, rows};
- }
- const atlases = {
- elf: makeHumanoidAtlas('#2ea44f'),
- guard: makeHumanoidAtlas('#2d6cdf'),
- evil: makeHumanoidAtlas('#d14b4b'),
- };
- // ===== World / Map (static layer to offscreen) =====
- const world = {
- w: 2000, h: 1400, // world pixels
- bg: null,
- zones: {
- HUMANS: {x:0, y:0, w:1000, h:700},
- PALACE: {x:1000, y:0, w:1000, h:700},
- ELVES: {x:0, y:700, w:1000, h:700},
- EVIL: {x:1000, y:700, w:1000, h:700},
- },
- obstacles: [], // rectangles for collisions
- };
- function buildWorld() {
- const oc=document.createElement('canvas'); oc.width=world.w; oc.height=world.h; const g=oc.getContext('2d');
- // zones tint
- g.fillStyle='#16243a'; g.fillRect(0,0,1000,700);
- g.fillStyle='#1b1b1f'; g.fillRect(1000,0,1000,700);
- g.fillStyle='#11281a'; g.fillRect(0,700,1000,700);
- g.fillStyle='#2b1719'; g.fillRect(1000,700,1000,700);
- // roads / paths
- g.fillStyle='#6b4e2e';
- g.fillRect(120, 120, 700, 18); // horizontal
- g.fillRect(1000-18, 120, 18, 460); // up-down to palace
- // town
- g.fillStyle='#d1a170'; roundRect(g, 120-30, 120-24, 60, 48, 6, true);
- // palace
- g.fillStyle='#cfcfcf'; roundRect(g, 1500-60, 350-40, 120, 80, 10, true);
- // elf huts
- for(let i=0;i<5;i++){ g.fillStyle='#8b5a2b'; roundRect(g, 250 + i*70, 1050 + (i%2)*40, 44, 34, 6, true); }
- // forest dots
- 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(); }
- // fort
- g.fillStyle='#666'; roundRect(g, 1500-40, 1100-32, 80, 64, 10, true);
- // mountains
- 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(); }
- // labels
- g.fillStyle='#9fb1c1'; g.font='16px system-ui';
- g.fillText('1: Люди / караваны', 20, 30);
- g.fillText('2: Дворец', 1020, 30);
- g.fillText('3: Лес эльфов', 20, 730+30);
- g.fillText('4: Форт Злого', 1020, 730+30);
- world.bg = oc;
- // obstacles rectangles (roads not blocking). We'll block major buildings.
- world.obstacles = [
- {x: 1500-60, y:350-40, w:120, h:80}, // palace
- {x: 120-30, y:120-24, w:60, h:48}, // town hall
- {x: 1500-40, y:1100-32, w:80, h:64}, // fort
- ];
- }
- 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(); }
- buildWorld();
- // ===== Entities =====
- const F = {ELF:'elf', GUARD:'guard', EVIL:'evil'};
- function makeUnit(f,x,y,isPlayer=false){
- return {
- id: Math.random(), f, x, y, vx:0, vy:0, dir:0, // 0 down,1 left,2 right,3 up
- frame:0, frameTimer:0,
- isPlayer, hp:100, dmg:24, range:28,
- inj:{ arm:false, leg1:false, leg2:false, eye:false },
- ai:{ t:0, dir:{x:0,y:0}, order:null, target:null }
- };
- }
- const game = {
- running:false, faction:null,
- gold:0,
- ents:[], caravans:[], loot:[],
- player:null,
- cam:{x:0,y:0,w:1000,h:600},
- start(f){
- this.running=true; this.faction=f; ui.setFaction(f);
- this.ents=[]; this.caravans=[]; this.loot=[];
- // spawn player
- const sp = f===F.ELF? {x:400,y:1100} : f===F.GUARD? {x:1500,y:300} : {x:1500,y:1100};
- this.player = makeUnit(f, sp.x, sp.y, true); this.ents.push(this.player);
- // load save
- const sv = Save.load(); if(sv){ this.gold=sv.gold||0; Object.assign(this.player.inj, sv.inj||{}); }
- // squads
- for(let i=0;i<6;i++) this.ents.push(makeUnit(F.GUARD, 1400 + (i%3)*30, 260 + Math.floor(i/3)*30));
- for(let i=0;i<6;i++) this.ents.push(makeUnit(F.ELF, 300 + (i%3)*30, 1000 + Math.floor(i/3)*30));
- for(let i=0;i<6;i++) this.ents.push(makeUnit(F.EVIL, 1420 + (i%3)*30, 1060 + Math.floor(i/3)*30));
- // caravans
- spawnCaravans(this);
- ui.log('Начало игры за: '+({elf:'Эльф',guard:'Страж',evil:'Злой'}[f]||f));
- },
- buy(it){
- const p=this.player; if(this.gold < it.cost){ ui.log('Недостаточно золота'); return; }
- if(it.id==='heal'){ p.hp=clamp(p.hp+40,0,100); }
- else if(it.id==='arm' && p.inj.arm){ p.inj.arm=false; }
- else if(it.id==='leg'){ p.inj.leg1=false; p.inj.leg2=false; }
- else if(it.id==='eye' && p.inj.eye){ p.inj.eye=false; }
- this.gold -= it.cost; Save.save({gold:this.gold, inj:p.inj}); ui.log('Куплено: '+it.title+' (-'+it.cost+' зол.)');
- }
- };
- // ===== Caravans =====
- function spawnCaravans(g){
- const path = [{x:180,y:120},{x:820,y:120},{x:820,y:320},{x:820,y:120}];
- for(let k=0;k<2;k++){
- const cart={x:path[0].x, y:path[0].y, seg:0, t:0, speed:90+Math.random()*30, guards:[]};
- const g1=makeUnit(F.GUARD, cart.x-20, cart.y); const g2=makeUnit(F.GUARD, cart.x+20, cart.y);
- cart.guards=[g1,g2]; g.ents.push(g1,g2); g.caravans.push(cart);
- }
- function stepCart(c, dt){
- const a=path[c.seg], b=path[(c.seg+1)%path.length];
- const dx=b.x-a.x, dy=b.y-a.y, dist=Math.hypot(dx,dy);
- 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)); } }
- c.x = a.x + dx*c.t; c.y = a.y + dy*c.t;
- 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; }
- }
- g.stepCaravans = (dt)=>{ for(const c of g.caravans) stepCart(c, dt); };
- }
- function dropGold(x,y,v){ game.loot.push({x,y,v}); }
- // ===== Update & Render =====
- let last=performance.now();
- function loop(){
- const now=performance.now(), dt=Math.min(0.033,(now-last)/1000); last=now;
- if(game.running){ update(dt); }
- draw();
- requestAnimationFrame(loop);
- }
- requestAnimationFrame(loop);
- function handleInput(dt){
- const p=game.player; if(!p) return;
- let mx=(keys['KeyD']||keys['ArrowRight']?1:0) - (keys['KeyA']||keys['ArrowLeft']?1:0);
- let my=(keys['KeyS']||keys['ArrowDown']?1:0) - (keys['KeyW']||keys['ArrowUp']?1:0);
- const len=Math.hypot(mx,my); if(len>0){ mx/=len; my/=len; }
- let speed = p.inj.leg2? 40 : p.inj.leg1? 80 : 140;
- p.vx=mx*speed; p.vy=my*speed;
- if(Math.abs(mx)>0.01 || Math.abs(my)>0.01){
- p.dir = Math.abs(mx) > Math.abs(my) ? (mx>0?2:1) : (my>0?0:3);
- p.frameTimer += dt*10; if(p.frameTimer>=1){ p.frame=(p.frame+1)%3; p.frameTimer=0; }
- } else {
- p.frame=1; // idle middle frame
- }
- // attack
- if(keys['Space'] || attackPressed){ attackPressed=false; if(!p.inj.arm) doAttack(p); }
- }
- function doAttack(src){
- let best=null, dmin=99999;
- for(const e of game.ents){
- if(e===src || e.hp<=0 || e.f===src.f) continue;
- const d=Math.hypot(e.x-src.x, e.y-src.y); if(d<dmin){ dmin=d; best=e; }
- }
- if(best && dmin < src.range){
- best.hp -= src.dmg;
- const r=Math.random();
- 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('Тяжёлая травма ноги!'); }
- else if(r<0.22 && best.hp>0){ best.inj.arm=true; if(best.isPlayer) ui.log('Потеряна правая рука!'); }
- else if(r<0.30 && best.hp>0){ best.inj.eye=true; if(best.isPlayer) ui.log('Выколот глаз!'); }
- if(best.hp<=0){ dropGold(best.x, best.y, 5+((Math.random()*8)|0)); }
- }
- }
- function tickAI(e, dt){
- // find closest enemy
- let target=null, d2min=999999;
- 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; } }
- let speed = e.inj.leg2? 35 : e.inj.leg1? 65 : 120;
- if(target && d2min < 220*220){
- const dx=target.x-e.x, dy=target.y-e.y, L=Math.hypot(dx,dy)||1;
- e.vx=(dx/L)*speed; e.vy=(dy/L)*speed;
- e.dir = Math.abs(dx) > Math.abs(dy) ? (dx>0?2:1) : (dy>0?0:3);
- e.frameTimer+=dt*10; if(e.frameTimer>=1){ e.frame=(e.frame+1)%3; e.frameTimer=0; }
- if(d2min < 26*26 && !e.inj.arm){ doAttack(e); }
- } else if(e.ai.order && e.ai.order.type==='attack'){
- const dx=e.ai.order.point.x - e.x, dy=e.ai.order.point.y - e.y, L=Math.hypot(dx,dy)||1;
- e.vx=(dx/L)*speed; e.vy=(dy/L)*speed;
- e.dir = Math.abs(dx) > Math.abs(dy) ? (dx>0?2:1) : (dy>0?0:3);
- e.frameTimer+=dt*8; if(e.frameTimer>=1){ e.frame=(e.frame+1)%3; e.frameTimer=0; }
- } else {
- // wander
- 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)}; }
- e.vx=e.ai.dir.x*speed*0.5; e.vy=e.ai.dir.y*speed*0.5;
- e.dir = Math.abs(e.vx) > Math.abs(e.vy) ? (e.vx>0?2:1) : (e.vy>0?0:3);
- e.frameTimer+=dt*6; if(e.frameTimer>=1){ e.frame=(e.frame+1)%3; e.frameTimer=0; }
- }
- }
- function collideRect(e, r){
- const half=12;
- const ex=e.x-half, ey=e.y-half, ew=half*2, eh=half*2;
- return ex < r.x+r.w && ex+ew > r.x && ey < r.y+r.h && ey+eh > r.y;
- }
- function integrate(e, dt){
- const nx = e.x + e.vx*dt, ny = e.y + e.vy*dt;
- // collisions
- let blocked=false;
- for(const r of world.obstacles){
- const test={x:nx,y:ny}; if(collideRect({x:nx,y:e.y}, r)) { e.vx=0; blocked=true; }
- if(collideRect({x:e.x,y:ny}, r)) { e.vy=0; blocked=true; }
- }
- e.x = clamp(e.x + e.vx*dt, 10, world.w-10);
- e.y = clamp(e.y + e.vy*dt, 10, world.h-10);
- }
- function update(dt){
- handleInput(dt);
- // AI + move
- for(const e of game.ents){ if(e.isPlayer || e.hp<=0) continue; tickAI(e, dt); }
- for(const e of game.ents){ integrate(e, dt); }
- // caravans
- if(game.stepCaravans) game.stepCaravans(dt);
- // loot pickup
- 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}); } }
- game.loot = game.loot.filter(l=>!l.dead);
- // camera follow
- const cam = game.cam; const p=game.player;
- cam.w = canvas.width / (window.devicePixelRatio||1); cam.h = canvas.height / (window.devicePixelRatio||1);
- 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);
- // EVIL commands
- if(game.faction===F.EVIL){
- if(keys['Digit1']){ orderAll(F.EVIL, {type:'attack', point:{x:1500,y:350}}); ui.log('Отрядам: штурм дворца!'); }
- if(keys['Digit2']){ orderAll(F.EVIL, {type:'attack', point:{x:400,y:1100}}); ui.log('Отрядам: удар по эльфам!'); }
- if(keys['Digit3']){ orderAll(F.EVIL, {type:'attack', point:{x:820,y:120}}); ui.log('Отрядам: на караваны!'); }
- }
- ui.updateHUD(game.player);
- }
- function orderAll(f, order){ for(const e of game.ents){ if(!e.isPlayer && e.f===f) e.ai.order=order; } }
- function draw(){
- const cam=game.cam;
- // bg
- ctx.clearRect(0,0,canvas.width,canvas.height);
- // blit world background
- if(world.bg) ctx.drawImage(world.bg, cam.x, cam.y, cam.w, cam.h, 0,0, cam.w, cam.h);
- // loot
- for(const l of game.loot){ drawCoin(l.x - cam.x, l.y - cam.y); }
- // entities
- for(const e of game.ents){
- const x=e.x - cam.x, y=e.y - cam.y;
- if(e.hp<=0){ // corpse
- 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;
- }
- drawDudeSprite(e, x, y);
- // tiny hp bar
- ctx.fillStyle='rgba(0,0,0,.5)'; ctx.fillRect(x-16, y-22, 32, 4);
- ctx.fillStyle='#7CFC7C'; ctx.fillRect(x-16, y-22, 32*(e.hp/100), 4);
- }
- }
- function drawCoin(x,y){
- ctx.save(); ctx.translate(x,y);
- ctx.fillStyle='#ffd54f'; ctx.beginPath(); ctx.arc(0,0,6,0,Math.PI*2); ctx.fill();
- ctx.strokeStyle='#a67c00'; ctx.lineWidth=1; ctx.stroke();
- ctx.restore();
- }
- function drawDudeSprite(e, x, y){
- const atlas = atlases[e.f];
- const fw=atlas.frameW, fh=atlas.frameH;
- const sx = e.frame * fw, sy = e.dir * fh;
- ctx.drawImage(atlas.img, sx, sy, fw, fh, x-24, y-36, 48, 72);
- }
- // ===== Menu / overlays =====
- document.querySelectorAll('#menu button[data-faction]').forEach(b=>{
- b.addEventListener('click', ()=>{
- document.getElementById('menu').style.display='none';
- const f=b.getAttribute('data-faction'); game.start(f);
- });
- });
- document.getElementById('startClose').addEventListener('click', ()=>{ document.getElementById('menu').style.display='none'; });
- document.getElementById('helpBtn').addEventListener('click', ()=>{ document.getElementById('help').style.display='flex'; });
- document.getElementById('helpClose').addEventListener('click', ()=>{ document.getElementById('help').style.display='none'; });
- document.getElementById('shopClose').addEventListener('click', ()=>{ ui.toggleShop(false); });
- // ===== Init log =====
- ui.log('WASD — движение • ЛКМ/Space — удар • B — лавка • H — помощь');
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment