Некромант: Готический замок
by NovaWizard24425 lines15.1 KB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>Некромант: Готический замок</title>
<style>
*{box-sizing:border-box;touch-action:manipulation}
body{margin:0;padding:10px;background:#100d20;color:#eee;font-family:system-ui,sans-serif}
.game{max-width:850px;margin:auto}
h2{font-size:22px}
.hud{display:flex;justify-content:space-between;gap:6px;flex-wrap:wrap;margin-bottom:8px}
.panel{background:#241a3d;border:1px solid #674b91;border-radius:8px;padding:8px}
canvas{width:100%;height:auto;display:block;aspect-ratio:16/9;background:#111126;border:1px solid #604584;border-radius:8px;touch-action:none;image-rendering:pixelated}
.controls{display:grid;grid-template-columns:100px minmax(0,1fr);gap:10px;align-items:center;margin-top:12px}
button{background:#392254;color:white;border:1px solid #a27ae2;border-radius:9px;padding:10px 5px;min-height:48px;font-weight:bold;font-size:13px}
.pad{width:100px;height:100px;border:2px solid #7356a8;background:#211937;border-radius:50%;position:relative;touch-action:none}
.knob{position:absolute;width:38px;height:38px;left:29px;top:29px;background:#9b7be0;border:2px solid #ddd0ff;border-radius:50%;pointer-events:none}
.actions{display:grid;grid-template-columns:1fr 1fr;gap:7px}
.actions button:last-child{grid-column:1/-1}
small{color:#c8b9e5}
@media(max-width:360px){.controls{grid-template-columns:82px minmax(0,1fr)}.pad{width:82px;height:82px}.knob{left:21px;top:21px}}
</style>
</head>
<body>
<div class="game">
<h2>☠ Некромант: Готический замок</h2>
<div class="hud">
<div class="panel">❤️ <b id="hp">100</b></div>
<div class="panel">🔮 <b id="mana">100</b></div>
<div class="panel">Волна <b id="wave">1/16</b></div>
<div class="panel" id="left">Враги: 5</div>
</div>
<canvas id="game" width="800" height="450"></canvas>
<div class="panel" id="status" style="margin-top:8px">Защити замок!</div>
<div class="controls">
<div class="pad" id="pad"><div class="knob" id="knob"></div></div>
<div class="actions">
<button id="magic">🔮 Магия −8</button>
<button id="summon">💀 Скелеты −70</button>
<button id="bat">🦇 Мышь −20</button>
</div>
</div>
<button id="restart" style="width:100%;margin-top:8px">🔄 Заново</button>
<p><small>WASD/стрелки — движение · Пробел — магия · Q — призыв · E — мышь.</small></p>
</div>
<script>
const $=id=>document.getElementById(id);
const c=$("game"),ctx=c.getContext("2d");
const W=800,H=450;
const CASTLE_IMG = new Image();
// CASTLE_IMG.src = 'castle.png';
const ZOMBIE_SPRITE = [
" GGGG ", " GGGGGG ", " GGWGWG ", " GGGGGG ", " GGGG ",
" BBBB ", " GBBBBG ", " GBBBBG ", " BBBB ", " B B ", " B B ", " G G "
];
const ZOMBIE_COLORS = {'G':'#6a9e5a','B':'#5c3a1e','W':'#ffffff'};
const BOSS_SPRITE = [
" GGGGGG ", " GGGGGGGG ", " GGGRGGRGG ", " GGGGGGGGG ",
" GGGGGG ", " GGGG ", " DDDDDD ", " DDDDDDDD ",
" DDDDDDDD ", " DD DD ", " DD DD ", " GG GG "
];
const BOSS_COLORS = {'G':'#3d7a3d','R':'#ff3333','D':'#2d2d2d'};
const SKELETON_SPRITE = [
" WWWW ", " WWWWWW ", " WW WW ", " WWWWWW ", " WWWW ",
" WWWW ", " W WW W ", " W WW W ", " WWWW ", " W W ", " W W ", " W W "
];
const SKELETON_COLORS = {'W':'#e0e0e0'};
const PLAYER_SPRITE = [
" HHHH ", " HHHHHH ", " HHHHHH ", " FFFF ", " FFFF ",
" PPPPPP ", " PPPPPPPP ", " PPPPPPPP ", " PPPPPPPP ", " PPPPPP ",
" PP PP ", " PP PP "
];
const PLAYER_COLORS = {'H':'#2d2d2d','F':'#e8d8b0','P':'#1a1a2e'};
function drawPixelArt(ctx, x, y, size, colorMap, pixelData) {
const rows = pixelData.length;
const cols = pixelData[0].length;
const px = size / Math.max(rows, cols);
const offsetX = -cols * px / 2;
const offsetY = -rows * px / 2;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const char = pixelData[r][c];
if (char === ' ' || char === '.') continue;
const color = colorMap[char];
if (color) {
ctx.fillStyle = color;
ctx.fillRect(offsetX + c * px, offsetY + r * px, px + 0.5, px + 0.5);
}
}
}
}
function drawCharacter(spriteData, colorMap, x, y, size, moving, phase=0) {
ctx.save();
ctx.translate(x, y);
if (moving) {
const t = phase * 10;
ctx.translate(0, -Math.abs(Math.sin(t)) * 2.5);
ctx.rotate(Math.sin(t) * 0.055);
}
drawPixelArt(ctx, 0, 0, size, colorMap, spriteData);
ctx.restore();
}
let p,enemies,skeletons,shots,bossArrows,hp,mana,wave,spawned,killed;
let mode,breakTime,spawnTimer,invincible,magicCD,summonCD,batCD,batTime;
let bossShootCD;
let joy={x:0,y:0},pointer=null,keys={},last=0;
let animTime=0;
let playerMoving=false;
const rand=(a,b)=>a+Math.random()*(b-a);
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const size=n=>n===1?5:n<=4?9:n<=7?17:n<=10?25:n===11?20:n<=13?26:n<=15?30:1;
function reset(){
p={x:400,y:320};enemies=[];skeletons=[];shots=[];bossArrows=[];
hp=100;mana=100;wave=1;spawned=0;killed=0;
mode="fight";breakTime=0;spawnTimer=0;invincible=0;
magicCD=summonCD=batCD=batTime=0;
bossShootCD=0;
animTime=0;playerMoving=false;
spawnEnemy();$("status").textContent="Защити замок!";hud();
}
function spawnEnemy(){
if(wave===16){
enemies.push({x:rand(180,620),y:280,r:30,hp:700,max:700,speed:25,boss:true});
}else{
let side=Math.floor(rand(0,4));
enemies.push({
x:side===0?8:side===1?792:rand(0,800),
y:side===2?270:side===3?355:rand(260,350),
r:12, hp:26.4, max:26.4, speed:43, boss:false
});
}
spawned++;
}
function hud(){
$("hp").textContent=Math.ceil(hp);
$("mana").textContent=Math.floor(mana);
$("wave").textContent=wave+"/16";
$("left").textContent=mode==="break"?"Перерыв: "+Math.ceil(breakTime)+"с":
mode==="fight"?(wave===16?"БОСС":Math.max(0,size(wave)-killed)):"—";
}
function nearest(x,y){
let best=null,d0=Infinity;
for(const e of enemies){let d=Math.hypot(e.x-x,e.y-y);if(d<d0){best=e;d0=d}}
return best;
}
function magic(){
if(mode!=="fight"||magicCD>0||mana<8)return;
let e=nearest(p.x,p.y);if(!e)return;
mana-=8;magicCD=.3;shots.push({x:p.x,y:p.y,e,damage:18});hud();
}
function summon(){
if(mode==="won"||mode==="lost"||summonCD>0||mana<70)return;
if(skeletons.length>0)return;
mana-=70;summonCD=1;
for(let i=0;i<3;i++){
let a=i*Math.PI*2/3;
skeletons.push({x:p.x+Math.cos(a)*15,y:p.y+Math.sin(a)*15,
dx:Math.cos(a),dy:Math.sin(a),spread:.25,hp:45,max:45,
damage:12,speed:84,cd:0});
}
hud();
}
function bat(){
if(mode!=="fight"||batCD>0||mana<20)return;
mana-=20;batCD=5;batTime=4;hud();
}
$("magic").onclick=magic;
$("summon").onclick=summon;
$("bat").onclick=bat;
$("restart").onclick=reset;
const pad=$("pad"),knob=$("knob");
function setJoy(e){
let r=pad.getBoundingClientRect();
let x=e.clientX-r.left-r.width/2;
let y=e.clientY-r.top-r.height/2;
let d=Math.hypot(x,y),radius=r.width*.25;
joy=d<4?{x:0,y:0}:{x:x/d*Math.min(1,d/radius),y:y/d*Math.min(1,d/radius)};
knob.style.transform=`translate(${joy.x*radius}px,${joy.y*radius}px)`;
}
pad.addEventListener("pointerdown",e=>{
if(pointer!==null)return;
pointer=e.pointerId;pad.setPointerCapture(pointer);setJoy(e);
});
pad.addEventListener("pointermove",e=>{if(e.pointerId===pointer)setJoy(e)});
function stopJoy(e){
if(e.pointerId===pointer){pointer=null;joy={x:0,y:0};knob.style.transform=""}
}
pad.addEventListener("pointerup",stopJoy);
pad.addEventListener("pointercancel",stopJoy);
pad.addEventListener("lostpointercapture",stopJoy);
window.addEventListener("keydown",e=>{
let k=e.key.toLowerCase();keys[k]=true;
if(["arrowup","arrowdown","arrowleft","arrowright"," "].includes(k))e.preventDefault();
if(e.repeat)return;
if(k===" ")magic();if(k==="q")summon();if(k==="e")bat();
});
window.addEventListener("keyup",e=>keys[e.key.toLowerCase()]=false);
window.addEventListener("blur",()=>{
keys={};pointer=null;joy={x:0,y:0};knob.style.transform="";
});
// === ИЗМЕНЕНО: скелеты всегда активны, даже во время перерыва ===
function updateSkeletons(dt){
for(const s of skeletons){
if(s.spread>0){
let t=Math.min(dt,s.spread);s.x+=s.dx*90*t;s.y+=s.dy*90*t;s.spread-=t;continue;
}
// Убрана проверка mode!=="fight" — скелеты двигаются всегда
s.cd-=dt;let e=nearest(s.x,s.y);if(!e)continue;
let d=Math.hypot(e.x-s.x,e.y-s.y)||1;
if(d>23){s.x+=(e.x-s.x)/d*s.speed*dt;s.y+=(e.y-s.y)/d*s.speed*dt}
else if(s.cd<=0){e.hp-=s.damage;s.cd=.65}
}
}
function update(dt){
if(mode==="won"||mode==="lost")return;
mana=Math.min(300,mana+7.5*dt);
magicCD=Math.max(0,magicCD-dt);
summonCD=Math.max(0,summonCD-dt);
batCD=Math.max(0,batCD-dt);
batTime=Math.max(0,batTime-dt);
invincible=Math.max(0,invincible-dt);
bossShootCD=Math.max(0,bossShootCD-dt);
let x=joy.x,y=joy.y;
if(keys.a||keys.arrowleft)x--;
if(keys.d||keys.arrowright)x++;
if(keys.w||keys.arrowup)y--;
if(keys.s||keys.arrowdown)y++;
const d=Math.hypot(x,y);
playerMoving = d>0.01;
if(playerMoving){
p.x=clamp(p.x+x/d*175*dt,15,785);
p.y=clamp(p.y+y/d*175*dt,250,352);
}
if(mode==="break"){
breakTime-=dt;
if(breakTime<=0){
if(wave===15){wave=16;startWave()}
else{wave++;startWave()}
}
updateSkeletons(dt);hud();return;
}
if(wave<16&&spawned<size(wave)){
spawnTimer-=dt;
if(spawnTimer<=0&&enemies.length<10){spawnEnemy();spawnTimer=.85}
}
for(const e of enemies){
let target=p,dist=Math.hypot(p.x-e.x,p.y-e.y);
for(const s of skeletons){
let ds=Math.hypot(s.x-e.x,s.y-e.y);
if(ds<dist){dist=ds;target=s}
}
if(dist>e.r+11){
e.x+=(target.x-e.x)/dist*e.speed*dt;
e.y+=(target.y-e.y)/dist*e.speed*dt;
}else if(target===p&&invincible<=0){
hp-=e.boss?23:5.5;
invincible=.65;
if(hp<=0){hp=0;mode="lost";$("status").textContent="Поражение! Нажми «Заново»."}
}
if(e.boss && bossShootCD<=0 && mode==="fight"){
bossShootCD = 1.8;
let dx = p.x - e.x, dy = p.y - e.y;
let dd = Math.hypot(dx, dy) || 1;
bossArrows.push({x: e.x, y: e.y, dx: dx/dd, dy: dy/dd, damage: 9, life: 3});
}
}
updateSkeletons(dt);
if(batTime>0){let e=nearest(p.x,p.y);if(e)e.hp-=12*dt}
for(const s of shots){
if(!enemies.includes(s.e)){s.dead=true;continue}
let dx=s.e.x-s.x,dy=s.e.y-s.y,d=Math.hypot(dx,dy)||1,step=440*dt;
if(d<step+s.e.r){s.e.hp-=s.damage;s.dead=true}
else{s.x+=dx/d*step;s.y+=dy/d*step}
}
shots=shots.filter(s=>!s.dead);
for(const a of bossArrows){
a.x += a.dx * 240 * dt;
a.y += a.dy * 240 * dt;
a.life -= dt;
if(a.life<=0){a.dead=true;continue}
if(Math.hypot(a.x-p.x, a.y-p.y) < 15 && invincible<=0){
hp -= a.damage;
invincible = 0.5;
a.dead = true;
if(hp<=0){hp=0;mode="lost";$("status").textContent="Поражение! Нажми «Заново»."}
}
}
bossArrows = bossArrows.filter(a=>!a.dead);
let before=enemies.length;
enemies=enemies.filter(e=>e.hp>0);killed+=before-enemies.length;
if(wave<16&&spawned>=size(wave)&&enemies.length===0){
mode="break";breakTime=8;$("status").textContent="Перерыв! Следующая волна через 8 секунд.";
}
if(wave===16&&enemies.length===0){
mode="won";$("status").textContent="🏆 Победа! Босс повержен!";
}
hud();
}
function startWave(){
enemies=[];shots=[];spawned=0;killed=0;mode="fight";spawnTimer=0;
bossArrows=[];bossShootCD=0;
for(let i=0;i<Math.min(3,size(wave));i++)spawnEnemy();
$("status").textContent=wave===16?"☠ Финальный босс!":"Волна "+wave+" началась!";
hud();
}
const stars=Array.from({length:90},()=>({x:rand(0,800),y:rand(0,245),r:rand(.5,1.8)}));
function draw(){
ctx.fillStyle="#111126";ctx.fillRect(0,0,W,H);
ctx.fillStyle="#b9a8db";for(const s of stars)ctx.fillRect(s.x,s.y,s.r,s.r);
ctx.fillStyle="#aaa1c7";ctx.beginPath();ctx.arc(625,90,39,0,7);ctx.fill();
ctx.fillStyle="#111126";ctx.beginPath();ctx.arc(644,75,35,0,7);ctx.fill();
if (CASTLE_IMG.complete && CASTLE_IMG.naturalWidth > 0) {
const imgRatio = CASTLE_IMG.naturalWidth / CASTLE_IMG.naturalHeight;
const canvasRatio = W / H;
let drawW, drawH, drawX, drawY;
if (imgRatio > canvasRatio) {
drawH = H; drawW = H * imgRatio;
drawX = (W - drawW) / 2; drawY = 0;
} else {
drawW = W; drawH = W / imgRatio;
drawX = 0; drawY = (H - drawH) / 2;
}
drawY += 60;
ctx.drawImage(CASTLE_IMG, drawX, drawY, drawW, drawH);
} else {
ctx.fillStyle="#272039";
for(let i=0;i<8;i++){
let xx=i*110;ctx.fillRect(xx,205,68,150);
ctx.beginPath();ctx.moveTo(xx,205);ctx.lineTo(xx+34,158);ctx.lineTo(xx+68,205);ctx.fill();
}
ctx.fillStyle="#a9e78b";for(let i=0;i<8;i++)ctx.fillRect(20+i*103,235+i%3*20,7,13);
}
ctx.fillStyle="#211a2c";ctx.fillRect(0,360,W,90);
ctx.fillStyle="#40583d";ctx.fillRect(0,356,W,7);
for(let i=0;i<70;i++){ctx.fillStyle=i%2?"#718654":"#354a3a";ctx.fillRect(i*12,356+i%4,7,4)}
for(let i=0;i<13;i++){
let gx=(i*71+25)%790,gy=382+i%4*12;
ctx.fillStyle="#454057";ctx.fillRect(gx,gy,15,23);ctx.fillRect(gx+4,gy-5,7,7);
}
for(const b of shots){
ctx.fillStyle="#70ffc3";ctx.shadowColor="#70ffc3";ctx.shadowBlur=12;
ctx.beginPath();ctx.arc(b.x,b.y,6,0,7);ctx.fill();ctx.shadowBlur=0;
}
for(const a of bossArrows){
ctx.save();
ctx.translate(a.x, a.y);
ctx.rotate(Math.atan2(a.dy, a.dx));
ctx.fillStyle="#ff5555";
ctx.shadowColor="#ff0000";ctx.shadowBlur=8;
ctx.fillRect(-8, -2, 16, 4);
ctx.beginPath();
ctx.moveTo(8, -4); ctx.lineTo(14, 0); ctx.lineTo(8, 4);
ctx.fillStyle="#ffaa00";
ctx.fill();
ctx.shadowBlur=0;
ctx.restore();
}
for(const s of skeletons){
drawCharacter(SKELETON_SPRITE, SKELETON_COLORS, s.x, s.y+8, 24, true, animTime+s.x*0.02);
ctx.fillStyle="#501c32";ctx.fillRect(s.x-12,s.y-14,24,3);
ctx.fillStyle="#b8f6a1";ctx.fillRect(s.x-12,s.y-14,24*Math.max(0,s.hp/s.max),3);
}
for(const e of enemies){
const sprite = e.boss ? BOSS_SPRITE : ZOMBIE_SPRITE;
const colors = e.boss ? BOSS_COLORS : ZOMBIE_COLORS;
drawCharacter(sprite, colors, e.x, e.y+8, e.boss?42:25, true, animTime+e.x*0.01);
ctx.fillStyle="#501c32";ctx.fillRect(e.x-22,e.y-27,44,5);
ctx.fillStyle=e.boss?"#ff365f":"#9bec68";
ctx.fillRect(e.x-22,e.y-27,44*Math.max(0,e.hp/e.max),5);
}
ctx.globalAlpha=invincible>0?.5:1;
drawCharacter(PLAYER_SPRITE, PLAYER_COLORS, p.x, p.y+10, 31, playerMoving, animTime);
if(batTime>0)ctx.fillText("🦇",p.x+25,p.y-15);
ctx.globalAlpha=1;
// === ИЗМЕНЕНО: убрана надпись "ПЕРЕРЫВ", оставлены только победа/поражение ===
if(mode==="won"||mode==="lost"){
ctx.fillStyle="#080817dd";ctx.fillRect(0,0,W,H);
ctx.textAlign="center";ctx.fillStyle=mode==="won"?"#85ffba":"#ff7897";
ctx.font="bold 30px system-ui";
ctx.fillText(mode==="won"?"ПОБЕДА!":"ПОРАЖЕНИЕ",W/2,H/2);
}
}
function frame(now){
let dt=Math.min(.04,(now-last)/1000||0);last=now;
animTime+=dt;
update(dt);draw();requestAnimationFrame(frame);
}
reset();requestAnimationFrame(frame);
</script>
</body>
</html>Game Source: Некромант: Готический замок
Creator: NovaWizard24
Libraries: none
Complexity: complex (425 lines, 15.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-novawizard24" to link back to the original. Then publish at arcadelab.ai/publish.