🎮ArcadeLab

2i RPG - Fantasy Adventure

by ShadowTurtle30
388 lines14.5 KB
▶ Play
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>2i RPG - Fantasy Adventure</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#151d29;color:white;font-family:monospace;text-align:center}
header{padding:12px;color:#ffdc83;font-size:28px;letter-spacing:5px}
.panel{background:#27374a;margin:8px;padding:10px;border-radius:9px}
canvas{display:block;width:100%;max-width:800px;margin:auto;image-rendering:pixelated}
button,select{font:bold 14px monospace;padding:12px;background:#394f67;color:white;border:2px solid #8196aa;border-radius:7px;cursor:pointer}
button:active{background:#6887a3}
button:disabled{opacity:.45;cursor:not-allowed}
.row{display:flex;justify-content:center;align-items:center;gap:7px;flex-wrap:wrap;margin:8px 0}
.stats{font-size:12px;line-height:1.9}
#quest{color:#ffdf88}
#log{font-size:12px;color:#d2e4f5;min-height:32px;line-height:1.5}
#screen{position:relative;max-width:800px;margin:auto}
#overlay{position:absolute;inset:0;background:rgba(13,23,36,.95);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:12px;z-index:2}
#overlay h2{color:#ffdf88;font-size:27px;margin:5px}
#overlay p{font-size:12px;line-height:1.7}
#classButtons{display:flex;gap:7px;flex-wrap:wrap;justify-content:center;margin:12px 0}
.classbtn.selected{border-color:#ffe58c;background:#785b32}
.big{font-size:19px;padding:14px 25px;background:#287748;border-color:#8ee9a5}
.retry{background:#8c483e;border-color:#ffb09e}
.skill{background:#6748a4;border-color:#c7a5ff}
</style>
</head>
<body>
<header>⚔ 2i RPG ⚔</header>
<div class="panel">
 <div id="status">AVVENTURA IN SOLITARIA</div>
 <div id="stats" class="stats"></div>
</div>

<div class="panel">
 <b>📜 MISSIONE: LIBERA IL VILLAGGIO</b>
 <p id="quest">Sconfiggi 5 mostri per salvare il villaggio!</p>
 <div id="progress">Mostri sconfitti: 0/5</div>
</div>

<div id="screen">
 <canvas id="game" width="800" height="400"></canvas>
 <div id="overlay">
  <h2 id="overlayTitle">⚔ 2i RPG ⚔</h2>
  <p id="overlayText">Scegli la tua classe e preparati all'avventura!<br>Hai tutto il tempo che vuoi.</p>
  <b>SCEGLI LA TUA CLASSE</b>
  <div id="classButtons">
   <button class="classbtn selected" data-class="warrior">🛡 Guerriero</button>
   <button class="classbtn" data-class="mage">🔮 Mago</button>
   <button class="classbtn" data-class="archer">🏹 Arciere</button>
  </div>
  <p id="classDescription">Molti punti vita e colpi potenti.</p>
  <button id="play" class="big">▶ GIOCA</button>
  <button id="retry" class="retry" style="display:none;margin-top:10px">↻ RIPROVA</button>
 </div>
</div>

<div class="panel">
 <div class="row">
  <button id="left">◀ SINISTRA</button>
  <button id="jump">⬆ SALTA</button>
  <button id="right">DESTRA ▶</button>
 </div>
 <div class="row">
  <button id="attack">⚔ ATTACCA [E]</button>
  <button id="skill" class="skill">✨ ABILITÀ [Q]</button>
  <button id="potion">🧪 POZIONE [R]</button>
 </div>
 <div id="log">Scegli la classe, poi premi GIOCA.</div>
</div>

<script>
const c=document.querySelector("#game"),ctx=c.getContext("2d");
const overlay=document.querySelector("#overlay");
const title=document.querySelector("#overlayTitle");
const overlayText=document.querySelector("#overlayText");
const stats=document.querySelector("#stats");
const status=document.querySelector("#status");
const quest=document.querySelector("#quest");
const progress=document.querySelector("#progress");
const log=document.querySelector("#log");

const floor=320,world=5000;
const classes={
 warrior:{name:"Guerriero",hp:150,mana:50,damage:27,cost:15,
  skill:"COLPO POTENTE",desc:"❤️ 150 vita · ⚔ Danni 27 · Attacco speciale potentissimo",color:"#f06464"},
 mage:{name:"Mago",hp:95,mana:120,damage:17,cost:25,
  skill:"PALLA DI FUOCO",desc:"🔮 120 mana · 🔥 Magia che danneggia più mostri",color:"#bd8bff"},
 archer:{name:"Arciere",hp:115,mana:75,damage:21,cost:18,
  skill:"FRECCIA MULTIPLA",desc:"🏹 Attacchi rapidi · Colpisce fino a 3 mostri",color:"#7de59a"}
};

let selectedClass="warrior",started=false,gameOver=false,done=false;
let defeated=0,xp=0,level=1,gold=0,potions=3,monsters=[],keys={};
let player={},nextId=0,lastTime=0,spawnClock=0;

function say(s){log.textContent=s}
function cls(){return classes[selectedClass]}

function resetGame(){
 const a=cls();
 player={x:100,y:floor-32,vy:0,ground:true,hp:a.hp,mana:a.mana,
  damage:a.damage,color:a.color,invincible:0};
 defeated=0;xp=0;level=1;gold=0;potions=3;
 done=false;gameOver=false;monsters=[];nextId=0;spawnClock=0;
 for(let i=0;i<8;i++)spawnMonster(300+i*180);
 quest.textContent="Sconfiggi 5 mostri per salvare il villaggio!";
 progress.textContent="Mostri sconfitti: 0/5";
 updateStats();draw();
}

function updateStats(){
 const a=cls();
 stats.innerHTML="🛡 "+a.name+" · ⭐ Livello "+level+
 " · XP "+xp+" · 🪙 Oro "+gold+"<br>"+
 "❤️ Vita "+Math.max(0,player.hp)+"/"+a.hp+
 " · 🔮 Mana "+player.mana+"/"+a.mana+
 " · 🧪 Pozioni "+potions;
 document.querySelector("#skill").textContent=
  "✨ "+a.skill+" ["+a.cost+" mana]";
 document.querySelector("#skill").disabled=
  !started||gameOver||player.mana<a.cost;
 document.querySelector("#attack").disabled=!started||gameOver;
 document.querySelector("#potion").disabled=!started||gameOver||potions<=0;
}

function selectClass(name){
 if(started)return;
 selectedClass=name;
 document.querySelectorAll(".classbtn").forEach(b=>
  b.classList.toggle("selected",b.dataset.class===name));
 document.querySelector("#classDescription").textContent=cls().desc;
 player.color=cls().color;
 player.hp=cls().hp;player.mana=cls().mana;
 player.damage=cls().damage;
 updateStats();draw();
}
document.querySelectorAll(".classbtn").forEach(b=>{
 b.onclick=()=>selectClass(b.dataset.class);
});

function startGame(){
 if(started&&!gameOver)return;
 resetGame();
 started=true;
 overlay.style.display="none";
 status.textContent="⚔ AVVENTURA IN CORSO · "+cls().name;
 say("La missione comincia! Sconfiggi 5 mostri.");
 updateStats();
}
document.querySelector("#play").onclick=startGame;

function showMenu(isRetry){
 started=false;
 overlay.style.display="flex";
 document.querySelector("#play").style.display=isRetry?"none":"inline-block";
 document.querySelector("#retry").style.display=isRetry?"inline-block":"none";
 document.querySelectorAll(".classbtn").forEach(b=>b.disabled=isRetry);
 title.textContent=isRetry?"💀 SEI STATO SCONFITTO":"⚔ 2i RPG ⚔";
 overlayText.textContent=isRetry
  ?"Il tuo eroe è caduto. Premi RIPROVA per tentare di nuovo!"
  :"Scegli la tua classe e preparati all'avventura! Hai tutto il tempo che vuoi.";
 status.textContent=isRetry?"SCONFITTA":"PRONTO PER GIOCARE";
 updateStats();draw();
}
document.querySelector("#retry").onclick=()=>{
 document.querySelectorAll(".classbtn").forEach(b=>b.disabled=false);
 startGame();
};

function spawnMonster(x){
 const type=Math.random()<.65?"slime":"goblin";
 monsters.push({id:nextId++,type,x,y:floor-32,
  hp:type==="slime"?35:55,max:type==="slime"?35:55,
  damage:type==="slime"?5:9,speed:type==="slime"?.55:.8,
  alive:true});
}

function awardXP(n){
 xp+=n;
 while(xp>=level*100){
  xp-=level*100;level++;
  player.hp=Math.min(cls().hp,player.hp+25);
  player.mana=Math.min(cls().mana,player.mana+20);
  say("⭐ LIVELLO "+level+"! Hai recuperato vita e mana.");
 }
}

function killMonster(m){
 if(!m.alive)return;
 m.alive=false;defeated++;gold+=5;
 awardXP(m.type==="goblin"?35:20);
 say("💥 Mostro sconfitto! +XP, +5 oro.");
 if(defeated>=5&&!done){
  done=true;gold+=50;awardXP(100);
  quest.textContent="🏆 MISSIONE COMPLETATA! Il villaggio è salvo!";
  say("🏆 VITTORIA! Hai liberato il villaggio. +50 oro e 100 XP!");
 }
 progress.textContent="Mostri sconfitti: "+defeated+"/5";
 updateStats();
}

function attack(){
 if(!started||gameOver)return;
 const m=monsters.filter(m=>m.alive&&Math.abs(m.x-player.x)<90)
  .sort((a,b)=>Math.abs(a.x-player.x)-Math.abs(b.x-player.x))[0];
 if(!m){say("Nessun mostro vicino. Avvicinati!");return;}
 m.hp-=player.damage;
 say("⚔ Colpo riuscito! Danni: "+player.damage);
 if(m.hp<=0)killMonster(m);
 updateStats();
}

function special(){
 if(!started||gameOver)return;
 const a=cls();
 if(player.mana<a.cost){say("🔮 Mana insufficiente!");return;}
 const targets=monsters.filter(m=>m.alive&&Math.abs(m.x-player.x)<210)
  .sort((a,b)=>Math.abs(a.x-player.x)-Math.abs(b.x-player.x));
 if(!targets.length){say("Nessun bersaglio vicino per "+a.skill+"!");return;}
 player.mana-=a.cost;
 let list,damage;
 if(selectedClass==="warrior"){list=targets.slice(0,1);damage=55;}
 else if(selectedClass==="mage"){list=targets.slice(0,3);damage=40;}
 else{list=targets.slice(0,3);damage=30;}
 list.forEach(m=>{m.hp-=damage;if(m.hp<=0)killMonster(m)});
 say("✨ "+a.skill+"! Hai inflitto "+damage+" danni a "+list.length+" bersagli.");
 updateStats();
}

function potion(){
 if(!started||gameOver)return;
 if(potions<=0){say("Hai finito le pozioni!");return;}
 if(player.hp>=cls().hp&&player.mana>=cls().mana){
  say("Vita e mana sono già al massimo!");return;
 }
 potions--;
 player.hp=Math.min(cls().hp,player.hp+45);
 player.mana=Math.min(cls().mana,player.mana+30);
 say("🧪 Pozione utilizzata: vita e mana recuperati.");
 updateStats();
}
document.querySelector("#attack").onclick=attack;
document.querySelector("#skill").onclick=special;
document.querySelector("#potion").onclick=potion;

function jump(){
 if(started&&!gameOver&&player.ground){
  player.vy=-10;player.ground=false;
 }
}
document.querySelector("#jump").onclick=jump;

function bindMove(id,key){
 const b=document.querySelector("#"+id);
 b.onpointerdown=e=>{e.preventDefault();keys[key]=true};
 ["pointerup","pointerleave","pointercancel"].forEach(ev=>
  b.addEventListener(ev,()=>keys[key]=false));
}
bindMove("left","left");bindMove("right","right");

window.addEventListener("keydown",e=>{
 const k=e.key.toLowerCase();
 if([" ","arrowleft","arrowright","arrowup"].includes(k))e.preventDefault();
 keys[k]=true;
 if((k===" "||k==="arrowup")&&!e.repeat)jump();
 if(k==="e"&&!e.repeat)attack();
 if(k==="q"&&!e.repeat)special();
 if(k==="r"&&!e.repeat)potion();
});
window.addEventListener("keyup",e=>keys[e.key.toLowerCase()]=false);

function draw(){
 const camera=Math.max(0,Math.min(world-800,(player.x||0)-350));
 const sky=ctx.createLinearGradient(0,0,0,400);
 sky.addColorStop(0,"#72bce6");sky.addColorStop(1,"#d5e9c2");
 ctx.fillStyle=sky;ctx.fillRect(0,0,800,400);
 ctx.fillStyle="#ffeb9b";ctx.fillRect(670,40,42,42);

 for(let x=Math.floor(camera/32);x<(camera+800)/32+1;x++){
  let sx=x*32-camera;
  ctx.fillStyle="#6bb64b";ctx.fillRect(sx,floor,32,12);
  ctx.fillStyle="#79533a";ctx.fillRect(sx,floor+12,32,100);
  ctx.strokeStyle="#60432f";ctx.strokeRect(sx,floor+12,32,100);
 }
 for(let x=160;x<world;x+=230){
  const sx=x-camera;if(sx<-60||sx>860)continue;
  ctx.fillStyle="#69452e";ctx.fillRect(sx,floor-48,15,48);
  ctx.fillStyle="#347d43";ctx.fillRect(sx-18,floor-88,52,42);
  ctx.fillStyle="#4a9a4c";ctx.fillRect(sx-8,floor-104,32,20);
 }

 const vx=100-camera;
 if(vx>-110&&vx<810){
  ctx.fillStyle="#d8bd85";ctx.fillRect(vx,floor-65,85,65);
  ctx.fillStyle="#91563e";ctx.fillRect(vx-8,floor-80,101,20);
  ctx.fillStyle="#634630";ctx.fillRect(vx+33,floor-33,20,33);
  ctx.fillStyle="white";ctx.font="11px monospace";
  ctx.fillText("VILLAGGIO",vx-5,floor-91);
 }

 for(const m of monsters){
  if(!m.alive)continue;
  const x=m.x-camera;if(x<-60||x>860)continue;
  if(m.type==="slime"){
   ctx.fillStyle="#4ed568";ctx.fillRect(x,m.y+8,30,24);
   ctx.fillStyle="#8af58e";ctx.fillRect(x+5,m.y+2,20,12);
   ctx.fillStyle="#173a29";ctx.fillRect(x+6,m.y+14,5,5);ctx.fillRect(x+20,m.y+14,5,5);
  }else{
   ctx.fillStyle="#53864a";ctx.fillRect(x+3,m.y+3,24,29);
   ctx.fillStyle="#7da85a";ctx.fillRect(x+5,m.y-2,20,16);
   ctx.fillStyle="#ffef9a";ctx.fillRect(x+8,m.y+4,4,4);ctx.fillRect(x+19,m.y+4,4,4);
  }
  ctx.fillStyle="#35212a";ctx.fillRect(x,m.y-13,34,6);
  ctx.fillStyle="#e95050";ctx.fillRect(x,m.y-13,34*Math.max(0,m.hp/m.max),6);
  ctx.fillStyle="white";ctx.font="10px monospace";
  ctx.fillText(m.type==="slime"?"SLIME":"GOBLIN",x-2,m.y-18);
 }

 const px=(player.x||0)-camera,py=player.y||floor-32;
 if(player.invincible<=0||Math.floor(performance.now()/100)%2===0){
  ctx.fillStyle=player.color||"#ed6464";ctx.fillRect(px,py+10,24,22);
  ctx.fillStyle="#efc39b";ctx.fillRect(px+3,py,18,16);
  ctx.fillStyle="#553b2d";ctx.fillRect(px+2,py,20,6);
  ctx.fillStyle="white";ctx.fillRect(px+5,py+6,4,4);ctx.fillRect(px+15,py+6,4,4);
 }
 ctx.fillStyle="#173044";ctx.font="12px monospace";
 ctx.fillText("2i RPG · X: "+Math.floor(player.x||0),12,20);
}

function update(t){
 const dt=Math.min(2,(t-lastTime)/16.67||1);lastTime=t;
 if(started&&!gameOver){
  const dx=((keys.right||keys.d||keys.arrowright)?1:0)-
           ((keys.left||keys.a||keys.arrowleft)?1:0);
  player.x=Math.max(0,Math.min(world-24,player.x+dx*3*dt));
  player.vy=Math.min(12,player.vy+.5*dt);
  player.y+=player.vy*dt;
  if(player.y+32>=floor){
   player.y=floor-32;player.vy=0;player.ground=true;
  }
  for(const m of monsters){
   if(!m.alive)continue;
   const d=Math.abs(m.x-player.x);
   if(d<350)m.x+=Math.sign(player.x-m.x)*m.speed*dt;
   if(d<27&&player.invincible<=0){
    player.hp=Math.max(0,player.hp-m.damage);
    player.invincible=55;
    say("💥 Un mostro ti ha colpito!");
    updateStats();
   }
  }
  if(player.invincible>0)player.invincible-=dt;
  spawnClock+=dt;
  if(spawnClock>180&&monsters.filter(m=>m.alive).length<7){
   spawnMonster(player.x+350+Math.random()*300);spawnClock=0;
  }
  if(player.hp<=0){
   gameOver=true;started=false;
   showMenu(true);
  }
 }
 draw();
 requestAnimationFrame(update);
}

function showMenu(isRetry){
 overlay.style.display="flex";
 document.querySelector("#play").style.display=isRetry?"none":"inline-block";
 document.querySelector("#retry").style.display=isRetry?"inline-block":"none";
 document.querySelectorAll(".classbtn").forEach(b=>b.disabled=isRetry);
 title.textContent=isRetry?"💀 SEI STATO SCONFITTO":"⚔ 2i RPG ⚔";
 overlayText.textContent=isRetry
  ?"Premi RIPROVA per ricominciare la missione."
  :"Scegli la tua classe e preparati all'avventura!";
}
selectClass("warrior");
showMenu(false);
resetGame();
requestAnimationFrame(update);
</script>
</body>
</html>

Game Source: 2i RPG - Fantasy Adventure

Creator: ShadowTurtle30

Libraries: none

Complexity: complex (388 lines, 14.5 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: 2i-multiplayer-shadowturtle30" to link back to the original. Then publish at arcadelab.ai/publish.