Pull to refresh

Roguelike/RPG на JavaScript (30 строк кода)

Reading time5 min
Views48K
После серии постов про реализацию простеньких игрушек на JavaScript в 30 строчек, решил попробовать себя в этом «соревновании». Посидев вечер, получилось создать «полноценную» Roguelike/RPG (я не слишком разбираюсь в жанрах, но вышло что-то в этом направлении). Заодно поизучал JavaScript (до этого на нем никогда не писал, как-то все C++ балуюсь).

image

Особенности:
  • Случайно генерируемый мир
  • Прокачка персонажа
  • 3 вида врагов и финальный босс
  • Инвентарь с бутылочками зелья и магазин для их пополнения


О игре

Вы играете за бравого рыцаря, который должен спасти принцессу от дракона. По дороге к пещере дракона с рыцарем происходят разные приключения вроде сражения с монстрами и посещения небольших поселений. После сражений с монстрами герой получает золото и жизненный опыт. Золото можно тратить на лечение и приобретение исцеляющих зелий в магазинчиках, что встречаются на пути. Жизненный опыт же повышает уровень героя, от чего растет его атака и защита. Цель игры — всесторонне подготовиться к схватке с драконом, а затем победить его и освободить принцессу.

Ссылка на fiddle.

Код

В качестве основы я взял код из этого поста, а затем «допилил» его до нужного мне состояния.

Исходный код (посмотреть на fiddle):
Исходный код
(function(elid, wi, he, exp, pot, gld, hp, lvl, cur_e, e_sz){
    var hit = function(){ evs[cur_e][8]-=lvl*4+6; if(evs[cur_e][8]<=0) {
        alert("Monster defeated and you got "+evs[cur_e][10]+"EXP and $"+evs[cur_e][11]);
        exp+=evs[cur_e][10];gld+=evs[cur_e][11]; while(exp>=lvl*10){exp-=lvl*10;lvl++;alert("Level Up!")}
        cur_e++; if(cur_e==e_sz) alert("Victory!") } else {
        hp-=Math.max(0,evs[cur_e][9]-lvl*2-1); if(hp<=0) alert("Game Over!") } }
    var use = function(){ if (pot>0){pot--;hp+=10;if(hp>100)hp=100} }
    var nxt = function(){ cur_e++ }, battle = ["Attack","Use Potion +10HP","Skip Battle",hit,use,nxt],
    canvas=document.querySelector(elid), ctx=canvas.getContext("2d"), evs=[], e_tp=[
    ["Shop", "Wizard provides his services", "Buy Potion $20", "Full Heal $100", "Leave",
     function(){ if(gld>=20){gld-=20;pot++} }, function(){ if(gld>=100){gld-=100;hp=100} }, nxt ],
    ["Skeleton", "A terrible skeleton on your way"].concat(battle,70,15,25,100),
    ["Goblin", "Green goblin wants to get your money"].concat(battle,50,10,15,70),
    ["Slime", "What the strange jelly monster?"].concat(battle,20,6,7,30),
    ["Dragon", "Omg! It is evil Dragon!","Attack","Use Potion +10HP","-",hit,use,,300,25,100,1000 ] ],
        q=e_tp.length-1; canvas.width=wi; canvas.height=he; for (var i=0;i<e_sz-1;i++)
     evs.push( e_tp[Math.floor(Math.random()*q)].slice(0) ); evs.push( e_tp[q].slice(0) );
    var game = setInterval(function(){ ctx.clearRect(0,0,wi,he);
        ctx.fillText("NanoRPG in 30 lines of JavaScript by ripatti",10,15);
        ctx.fillText("LVL "+lvl+"  HP "+hp+"/100  EXP "+exp+"/"+lvl*10+"  ATK "+(lvl*4+6)+
         "  DEF "+(lvl*2+1)+"  Gold $"+gld+"  Potions "+pot,10,30);
        for (var i=0;i<e_sz;i++) ctx.fillText((i==e_sz-1||i<=cur_e)?evs[i][0]:"??",i*50+15,70);
        ctx.fillText("@",cur_e*50+25,60); ctx.fillText(evs[cur_e][1],20,100);
        if (evs[cur_e].length>8) ctx.fillText("Enemy HP "+evs[cur_e][8],250,100);
        for (var i=0;i<3;i++) { ctx.strokeRect(i*120+5,120,110,20);
            ctx.fillText(evs[cur_e][i+2],i*120+10,133); } }, 100);
    document.addEventListener('click', function(e){ for (var i=0;i<3;i++)
        if (i*120+5<=e.pageX && e.pageX<i*120+115 && 120<=e.pageY && e.pageY<140)
            if (hp>0) evs[cur_e][i+5]() }, false);
})("#canvas",365,150,0,3,100,100,1,0,7);


Несколько более читаемая версия в 50 строчек (посмотреть на fiddle):
Исходный код
(function(elid, wi, he, exp, pot, gld, hp, lvl, cur_e, e_sz){
    var hit = function(){
        evs[cur_e][8]-=lvl*4+6;
        if (evs[cur_e][8]<=0) {
            alert("Monster defeated and you got "+evs[cur_e][10]+"EXP and $"+evs[cur_e][11]);
            exp+=evs[cur_e][10]; gld+=evs[cur_e][11];
            while (exp>=lvl*10) { exp-=lvl*10; lvl++; alert("Level Up!") }
            cur_e++; if (cur_e==e_sz) alert("Victory!")
        } else {
            hp-=Math.max(0,evs[cur_e][9]-lvl*2-1);
            if(hp<=0) alert("Game Over!")
        }
    }
    var use = function(){ if (pot>0){pot--;hp+=10;if(hp>100)hp=100} }
    var nxt = function(){ cur_e++ }
    var battle = ["Attack", "Use Potion +10HP", "Skip Battle", hit, use, nxt];
    var canvas=document.querySelector(elid), ctx=canvas.getContext("2d");
    canvas.width=wi; canvas.height=he;
    var evs=[], e_tp=[
    ["Shop", "Wizard provides his services", "Buy Potion $20", "Full Heal $100", "Leave",
        function(){ if(gld>=20){gld-=20;pot++} },
        function(){ if(gld>=100){gld-=100;hp=100} }, nxt ],
    ["Skeleton", "A terrible skeleton on your way"].concat(battle,70,15,25,100),
    ["Goblin", "Green goblin wants to get your money"].concat(battle,50,10,15,70),
    ["Slime", "What the strange jelly monster?"].concat(battle,20,6,7,30),
    ["Dragon", "Omg! It is evil Dragon!","Attack","Use Potion +10HP","-",hit,use,,300,25,100,1000 ] ];
    var q=e_tp.length-1;
    for (var i=0;i<e_sz-1;i++) evs.push( e_tp[Math.floor(Math.random()*q)].slice(0) );
    evs.push( e_tp[q].slice(0) );
    var game = setInterval(function(){
        ctx.clearRect(0,0,wi,he);
        ctx.fillText("NanoRPG in 30 lines of JavaScript by ripatti",10,15);
        ctx.fillText("LVL "+lvl+"  HP "+hp+"/100  EXP "+exp+"/"+lvl*10+"  ATK "+(lvl*4+6)+
         "  DEF "+(lvl*2+1)+"  Gold $"+gld+"  Potions "+pot,10,30);
        for (var i=0;i<e_sz;i++) ctx.fillText((i==e_sz-1||i<=cur_e)?evs[i][0]:"??",i*50+15,70);
        ctx.fillText("@",cur_e*50+25,60);
        ctx.fillText(evs[cur_e][1],20,100);
        if (evs[cur_e].length>8) ctx.fillText("Enemy HP "+evs[cur_e][8],250,100);
        for (var i=0;i<3;i++) {
            ctx.strokeRect(i*120+5,120,110,20);
            ctx.fillText(evs[cur_e][i+2],i*120+10,133);
        }
    }, 100);
    document.addEventListener('click', function(e){
        if (hp>0)
            for (var i=0;i<3;i++)
                if (i*120+5<=e.pageX && e.pageX<i*120+115 && 120<=e.pageY && e.pageY<140)
                    evs[cur_e][i+5]()
    }, false);
})("#canvas",365,150,0,3,100,100,1,0,7);


Код на html и css я не учитывал, но если кому интересно: html — 1 строка кода, css — 4.

Заключение

Спасибо agegorin за гоночку, DjComandos за змейку и linoleum за арканоид. Было интересно почитать исходный код и на основе этого написать что-то свое.

UPD Roguelike/RPG в 4Кб кода на JavaScript

Товарищ shvedovka предложил сделать цикличную игру с усложнением, что я, собственно, и сделал. В 30 строк оно уже совсем не лезло и я взял другой ограничение: кода должно быть не более 4 килобайт (это согласуется с замечанием lolmaus, да и мне лично больше нравится). Привинтив цикличную игры, я добавил еще несколько событий — набил игру контентом под самые ограничения.

А именно:
  • Арена, где можно, тренируясь, обменивать деньги на опыт
  • Кузница, где можно улучшать меч и доспехи
  • +2 вида врагов — теперь их итого 5 видов

Небольшие изменения в генерации миров:
  • Некоторые монстры и здания появляются только если пройти игру несколько раз
  • Одна из двух локаций перед замком дракона обязательно будет магазином

И много мелких изменений в плане балансировки всего и вся. И да… теперь принцессу действительно можно спасти.

Ссылка на эту версию игры на fiddle. Получилось довольно хардкорно.

Ну и код, конечно. Форматирование не совсем наглядное — я поудалял немного пробелов, чтобы уложиться в 4000 символов. В принципе, код можно еще сильнее ужать, чтобы впихнуть еще одно событие, например (была идея сделать что-то вроде казино). Но мне уже лень этим заниматься.

На этом работу над игрой я считаю законченной.
Tags:
Hubs:
+47
Comments21

Articles