Войти на сайт

Авторизация, ждите ...
×

ТЕМА: [RMMV] Пишем свой первый плагин

[RMMV] Пишем свой первый плагин 8 года 5 мес. назад #86266

  • Mur
  • Mur аватар
  • Вне сайта
  • Светлый дракон
  • Мур? Мур! Мур.
  • Сообщений: 574
  • Спасибо получено: 1448
  • 2 место ОраторПрограммист JavaScript УчительОрганизатор конкурсовДаритель Стимкея
Всем доброй ночи. :whistle:

Сегодня мой день был потрачен не зря, ибо мне удалось написать свой первый небольшой плагин для нового RPG Maker MV. Сразу скажу, что ни разу не профессионал, занимаюсь в свободное от кухни время и только только разбираюсь во всём это безобразии. Хотя есть некоторый опыт скриптоковыряния VX Ace, что конечно же мне помог и тут. В любом случае спешу поделится своими изысканиями и надеюсь они будут ещё полезны кому-нибудь. :blush:

Начать мне хотелось бы с редактора. Как вы уже наверное заметили, в отличии от VX Ace в MV своего редактора нет. Скрипты лежат в открытом виде и можете спокойно их редактировать в любом текстовом редакторе. В одном из видеоуроков человек порекомендовал brackets. Опыта у меня не много в подобного рода редакторах, но мне понравился. Особенно очень удобно когда начинаешь что-то писать, а тебе бац и сразу подсказки. Мелочь, а приятно. При желании можно выбрать удобную цветовую тему. Так что можете тоже попробовать совершенно бесплатно.

brackets.png


Для начала о том, что мы будем сегодня делать. Этот простой плагин рисует на экране небольшое окошко с иконкой и цифрой. Данные берутся из переменных (Variables) RPG Maker. А спомощью ключа (switch) можно включить или спрятать отображение данного окна. Теперь для чего? Например NPC даёт герою задание, что нужно собрать 20 шишек. Понятно, что задание не сильно увеселительное, но можно игроку немного скрасить жизнь. Отображая например цифрами, сколько ему ещё осталось мучится. Выглядит это примерно так:


screenshot.png



Для того, что бы пользователь мог выбрать произвольные ячейки (Variables) и ключ (Switch) для отображения, мы выведем эти параметры в настройки нашего плагина. В принципе можно вынести и размер окна и его положение на экране, но мне бы пока не хотелось так всё сразу запутывать.

Итак, создадим новый файл ItemOnMap.js в папке нашего проекта, а именно js\plugins. Этого уже достаточно, что бы он уже появился в списке доступных плагинов, однако всё же стоит добавить его описание:
//=============================================================================
// ItemOnMap.js
//=============================================================================

Здесь название нашего файла. Не могу сказать, что это нужно наверняка, но во всех плагинах под MV примерно такое-же оформление. Возможно система по этому определяет, что это именно наш плагин а ни чей-нибудь ещё.
/*:
Открываем комментарий и описываем различные параметры:

 * @plugindesc Отображает на экране количество предметов в ячейке
 * @author Mur
 *
Далее указывается описание плагина (@plugindesc) и собственно кто автор (@author)

 * @param enableSwitchId
 * @desc Показать/скрыть (номер ключа)
 * @default 1
 *
Затем следуют перечисления параметров нашего плагина. Тут надо быть очень внимательными, ибо эти название затем используются в скрипте. И если что-то напутать, ничего не получится. Один параметр состоит из трёх частей, собственно его названия @param (на английском языке и честно не знаю, можно ли писать на русском), описание данного параметра @desc (не знаю почему, но описание на русском не отображается) и значение по умолчанию @default. Данный параметр указывает какой ключ (switch) будет разрешать или запрещать отображение данного окошка.

 * @param itemVarId
 * @desc Иконка предмета (номер переменной) 
 * @default 1
 *
Здесь указывает номер переменной (Variable) в которой хранится номер предмета, который должен собирать наш главный герой. Иконка этого предмета и отображается в нашем окошке.

 * @param countVarId
 * @desc Количество предметов (номер переменной) 
 * @default 2
Ну и завершает это всё последний параметр, который так же указывает на номер переменной (Variable) в которой хранится количество собранных предметов.

*/
Закрываем комментарий и далее следует уже наш скрипт самого плагина.

(function() {
Такой строчкой начинаются почти все плагины, которая собственно и сообщает системе, что сейчас мы будет творить!

var parameters = PluginManager.parameters('ItemOnMap');
Для начала нам нужно добраться до настроек нашего плагина. Не знаю как это всё связано, но обратите внимание, что наш файл, первый кусочек и здесь название плагина везде одинаковое, в данном случае это «ItemOnMap».

    var enableSwitchId = Number(parameters['enableSwitchId']);
    var itemVarId = Number(parameters['itemVarId']);
    var countVarId = Number(parameters['countVarId']);
Здесь мы получаем значение наших параметров. Честно не знаю, зачем тут написано Number, но в других скриптах сделано так же, так что видимо так надо :)

А так это выглядит когда мы подключаем наш плагин:

params.png


    //Обновление всех окон
    var _Scene_Map_createAllWindows = Scene_Map.prototype.createAllWindows;
    Scene_Map.prototype.createAllWindows = function() {
        _Scene_Map_createAllWindows.call(this);
        this._itemOnMap = new ItemOnMap(10,10, 120, 60);
        this.addWindow(this._itemOnMap);
    };
Вот тут очень важный и сложный момент. Для того, что бы наше окно постоянно отображалось. Нам нужно втиснутся в основную функцию создания всех окон (createAllWindows). Если вы писали скрипты (или хотя бы пытались что-то изменить в чужих, как я например) под VX Ace, то наверняка заметили схожий приём. Когда главная функция системы подменялась на нашу, а потом вызывалась внутри с такими же параметрами. Ну не суть важно, я думаю если кому интересно он спросит у знатоков, а тут просто для пояснения.

Сначала создаём «копию» основной функции системы:
var _Scene_Map_createAllWindows = Scene_Map.prototype.createAllWindows;

Затем заменяем её уже своей фунцией:
Scene_Map.prototype.createAllWindows = function() {

внутри которой вызываем настоящую, что бы ничего не поломалось:
_Scene_Map_createAllWindows.call(this);

И затем уже добавляем создание нашего окошка:
this._itemOnMap = new ItemOnMap(10,10, 120, 60);
Первые два числа это координаты где на экране будет наше окно, а два другие его соответственно ширина и высота.

this.addWindow(this._itemOnMap);
Затем это окно добавляется в систему и далее собственно уже функции нашего окна:
    function ItemOnMap() {
        this.initialize.apply(this, arguments);
    }
Честно скажу, назначение этого кусочка мне до конца не ясно. Однако сразу скажу, что без него не работает и кроме того опять же очень важно название — «ItemOnMap»!! Оно идёт везде одинаковое, ошибётесь на 1 букву будут ошибки.

    ItemOnMap.prototype = Object.create(Window_Base.prototype);
    ItemOnMap.prototype.constructor = ItemOnMap;
Дальше, если я правильно понимаю, копируются все параметры, свойства и собственно функции отрисовки системного окна в наш ItemOnMap. Дальше мы можем в нём менять всё что душе угодно, не боясь что это отобразиться свойствах всех остальных окошек.

    ItemOnMap.prototype.standardPadding = function() {
        return 0;
    };
Например вот, по умолчанию отступ у всех окон 18px. Для того что бы наш текст и иконка были видны его стоит задать равным 0.

    ItemOnMap.prototype.initialize = function(x, y, width, height) {
        Window_Base.prototype.initialize.call(this, x, y, width, height);
        this._id = 1;
    };
Далее, как я понимаю идёт инициализация нашего окна, с такими же параметрами как у главного.

Ну и самое интересное. Окно появилось, и остаётся только вывести в него информацию:
    ItemOnMap.prototype.update = function() {
Для этого необходимо в функции update написать то, что мы хотим вывести в нашем окне.

        this.contents.clear();
Для начала его надо очистить, если этого не сделать всё будет накладываться друг на дружку и в конце концов будет выглядеть ужасно.

        if ($gameSwitches.value(enableSwitchId)) {
Далее мы проверяем, разрешено ли показывать наше окно, если да, то:

            this.show();
Покажем окно

            this.resetTextColor();
Установим настройки (цвет, размер) текста по умолчанию

            var itemId = $gameVariables.value(itemVarId);
Берём номер предмета, который нужно отобразить:

            if (itemId == 0) {
                itemId = 1;
            }

Проверяем, что бы он не был равен 0, иначе будет ошибка.

            this.drawIcon($dataItems[itemId].iconIndex, 15, 15);
Рисуем нашу иконку. $dataItems[itemId].iconIndex номер иконки предмета, и две последующие цифры это координаты x,y внутри нашего окна.

            this.drawTextEx(":" + $gameVariables.value(countVarId), 52, 15);
Далее выводим наше число из переменной, и опять же два последующих числа это координаты x,y внутри нашего окна, но только уже для вывода текста.

        } else {
Если же показывать окно нельзя, то

            this.hide();
Скроем его и закончим данную функцию.

        }
    }

Собственно на этом всё, остаётся только не забыть закончить главную функцию плагина:
})();

Как видите ничего сложного почти нет. Хотя по правде говоря было очень не просто сходу разобраться со всем этим, и если бы не потраченные дни предварительного разбирания и ковыряния VX Ace, мне бы не удалось это всё сделать так «быстро». Однако надо сказать огромное спасибо авторам, что так же как в VX Ace, RMMV название функций и переменных очень схожи, что конечно хоть и не сильно, но всё же упрощает написание плагинов под RMMV.

Если вдруг будут какие-то вопросы, постараюсь в меру своих сил ответить :blush:

p.s. огромное спасибо всем кто мне помогал и помогает разбираться с премудростями RPG Maker, как здесь на форуме, так и в нашем горячо любимом чатике. Как видите мои глупые вопросы наконец вылились во что-то если не серьёзное, так во что-то не плохое это точно.

Полный текст плагина «ItemOnMap.js

Вложенный файл:

Имя файла: ItemOnMap.zip
Размер файла: 1 KB
Последнее редактирование: 8 года 5 мес. назад от Mur.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: AnnTenna, Kerotan, Ren310, strelokhalfer, Dprizrak1, Lucin, caveman, Демий, KageDesu, EvilCat и еще 5 пользователей

[RMMV] Пишем свой первый плагин 8 года 5 мес. назад #86267

  • EvilCat
  • EvilCat аватар
  • Вне сайта
  • Просветлённый
  • Сообщений: 469
  • Спасибо получено: 850
  • 2 место 3 место ГотвУчитель
Хочется объяснить, что это за загадочная конструкция, которая есть почти во всех плагинах:
(function() {
 // тут собственно весь плагин
})();

... и почему она нам показывает рожи и подмигивает нам.

Это называется "самовызываемой анонимной функцией".

function() { ... } - это функция. У неё нет имени, иначе было бы function <имя> () { ... }, поэтому она анонимная. Она заключена в скобки: (function() { ... }). После скобок добавлены ещё две скобки (), которые вызывают предыдущий член выражения. Поскольку предыдущий член выражения - это функция, то вызывается функция. Но если бы она сама не была заключена в скобки, то строчка не воспринималась бы как выражение. Поскольку функция ничему не присваивается и имени у неё нет, то, отработав, она перестаёт существовать. Завершающая точка с запятой нужна для обозначения конца строчки (хотя в принципе её можно и не ставить).

Зачем нужна эта страшная конструкция? Почему она настолько обязательна, что с неё начинаются почти все плагины?

Дело в том, что первоначально Яваскрипт на веб-страницах выполнял очень ограниченную роль и обходился без заморочек - включая такую заморочку как область видимости переменных. Поэтому если без такой обёртки в своём плагине написать, например, var parameters = PluginManager.parameters('ItemOnMap'), то это создаст глобальную переменную parameters, содержащую параметры плагина ItemOnMap. Все переменные, объявленные с var вне функции объявляются в глобальном пространстве. Поэтому-то это и опасно: так можно либо переписать нужную другому модулю переменную, либо же, наоборот, другой модуль может переписать вашу. Единственный способ "защитить" переменные в Яваскрипте был использование такой вот анонимной функции. Переменные, объявленные внутри функции снаружи не видны и ничего не затирают... Зато код внутри функции прекрасно их видит, даже код функций, создаваемых внутри анонимной функции.

Короче! В Яваскрипте 6, который используется Мейкером MV, это больше не требуется. Сообщество Яваскрипта не хотело никого заставлять пользоваться эзотерической конструкцией, они просто не подумали, что области видимости могли понадобиться. А теперь, когда стало ясно, что это так, они добавили этот функционал в язык.

Вот как можно сделать вместо самовызываемой анонимной функции:
"use strict"; // инструктирует Яваскрипт, что к коду следует применять более требовательные стандарты, включая области видимости.
 
{ // блок открывается.
	let parameters = PluginManager.parameters('ShadowedCharacters'); // нужно использовать let вместо var, чтобы переменная объявлялась в рамках блока.
 // далее остальной код плагина
} // блок закрывается
// вне блока глобальная переменная parameters свободна!
 

Код Мейкера и многих плагинов не использует этого по разным причинам: по привычке, из-за незнания (Яваскрипт 6 сравнительно новый), из-за опасения, что среда будет поддерживать только 5-ю часть... Тем не менее, сейчас Яваскрипт 6 вошёл в широкое пользование и поэтому поддерживается в MV, так что кому больше нравится без анонимной функции - может спокойно без неё обойтись.

Пояснения по неясным местам
Честно не знаю, зачем тут написано Number, но в других скриптах сделано так же, так что видимо так надо

Когда пользователь вводит параметры плагина, он не может указать, вводит он число или строку. 300 - это число триста или это название фильма? Допустим, Яваскрипту придётся складывать два параметра, не зная, что они из себя представляют: 300+007 - это будет 307 или строка '300007'? Поэтому когда нужно получить из параметра число, его нужно сделать таковым в явном виде. В Яваскрипте есть несколько способов преобразовать строку в число, например, parseInt('2+2') даёт 4, а Number('2+2') более прямолинейный и возвратит NaN (Not a Number - "не число", потому что есть лишние символы).



function ItemOnMap() {
        this.initialize.apply(this, arguments);
    }

Каждая функция в Яваскрипте - это объект (так же, как в Руби). И у этих объектов-функций есть такие методы как call и apply. Здесь вызывается метод apply у функции, содержащейся в this.initialize. Метод call в качестве первого параметра принимает объект, который в рамках исполнения функции будет считаться за this, а далее можно перечислить аргументы. Метод apply делает то же самое, но принимает не открытый список аргументов, а массив аргументов. Например, это одно и то же (во втором случае квадратные скобки обозначают запись массива):
this.some_method.call(obj, 1, 99, 'hello');
this.some_method.apply(obj, [1, 99, 'hello'] );

Ну а arguments - в рамках выполнения метода особый массивоподобный объект, содержащий все аргументы. Если нужно вызвать метод с такими же аргументами и контекстом (this), с которыми был вызван данный, пишут:
this.some_method.apply(this, arguments);

ItemOnMap.prototype = Object.create(Window_Base.prototype);
    ItemOnMap.prototype.constructor = ItemOnMap;

Таким образом в Яваскрипте вплоть до 5-й версии производилось наследование. Классы в Яваскрипте организовывались традиционно так:
function НазваниеКласса(аргумент, аргумент)
// функция-констурктор класса, применяется с ключевым словом new, например: var obj=new НазваниеКласса(арг, арг);
{
	// код конструктора (в MV вызывается почти везде метод initialize)
}
НазваниеКласса.prototype = Object.create(НазваниеРодительскогоКласса.prototype);
// создаёт цепочку схем: объекты класса НазваниеКласса будут иметь схему НазваниеКласса.prototype, который в свою очередь будет иметь схему НазваниеРодительскогоКласса.prototype. Если запросить у объекта метод или параметр, то он сначала поищет в своих личных свойствах, затем - в свойствах своего прототипа, НазваниеКласса.prototype, затем в свойствах НазваниеРодительскогоКласса.prototype (прототипа своего прототипа) и так далее.
 
НазваниеКласса.prototype.constructor = НазваниеКласса;
// проблема в том, что теперь у него и другая функция-конструктор, такая же, как у родительского класса! данная строчка исправляет это.

В Яваскрипте 6-й версии уже есть привычный синтаксис объявления и описания классов. Кроме того, многие кодеры на Яваскрипте используют самодельные функции, позволяющие унаследовать один класс от другого и даже добавить до кучи дополнительные наборы черт, например. После этого достаточно написать
extend(НазваниеКласса, НазваниеРодительскогоКласса, Плюшка, Плюшка2);

Чтобы класс НазваниеКласса был дочерним к НазваниеРодительскогоКласса и в дополнение имел свойства из Плюшка и Плюшка2. Уверена, где-нибудь в Yanfly Core должен быть такой метод.
Последнее редактирование: 8 года 5 мес. назад от EvilCat.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: Kerotan, DK, Ren310, strelokhalfer, Dprizrak1, caveman, Демий, Волчонок, Lipton, Mur и еще 3 пользователей

[RMMV] Пишем свой первый плагин 8 года 5 мес. назад #86272

  • Mur
  • Mur аватар
  • Вне сайта
  • Светлый дракон
  • Мур? Мур! Мур.
  • Сообщений: 574
  • Спасибо получено: 1448
  • 2 место ОраторПрограммист JavaScript УчительОрганизатор конкурсовДаритель Стимкея
EvilCat пишет:
Хочется объяснить

Спасибо огромное! :whistle:
Администратор запретил публиковать записи гостям.

[RMMV] Пишем свой первый плагин 8 года 5 мес. назад #86273

  • Волчонок
  • Волчонок аватар
  • Вне сайта
  • Просветлённый
  • Волчонок
  • Сообщений: 277
  • Спасибо получено: 247
  • 2 место 3 место в Кодировке3 местоОрганизатор конкурсов
EvilCat пишет:
Хочется объяснить

Профессиональный подход, лёгкий слог и ни миллиграмма воды. Крутое объяснение...
Ленивый волчонок с большими амбициями

Рассказ "Рождение мага", периодически обновляется
РПГ История Егеря. Основной текущий проект.

ВНИМАНИЕ: Спойлер! [ Нажмите, чтобы развернуть ]
Администратор запретил публиковать записи гостям.

[RMMV] Пишем свой первый плагин 8 года 5 мес. назад #86281

  • EvilCat
  • EvilCat аватар
  • Вне сайта
  • Просветлённый
  • Сообщений: 469
  • Спасибо получено: 850
  • 2 место 3 место ГотвУчитель
Мне задали в привате вопрос, и я хотела бы кое-что объяснить, чтобы Яваскрипт, возможно, сразу стал понятнее.

Дело в том, что Руби - это преимущественно классовый язык. В нём объявляются классы, создаются объекты-экземпляры классов, а некоторые классы являются наследниками других классов.

А Яваскрипт - это преимущественно прототипный язык. В нём у объектов есть не класс, а прототип, схема. Когда у объекта требуешь "выполни метод Х", то объект сначала ищет среди собственных, индивидуальных свойств, а если не находит - то ищет в прототипе. В классовых языках объект ищет метод Х в объявлении класса. У прототипных языков тоже есть наследование, поскольку у прототипов могут быть прототипы (именно эту роль выполняет строчка НазваниеКласса.prototype = Object.create(НазваниеРодительскогоКласса.prototype) - она создаёт протитип класса так, чтобы у этого прототипа был собственный прототип, такой же, как у родительского класса. Если объект не нашёл метод Х ни у себя, ни в своём прототипе, то поиск продолжается в прототипе прототипа, и далее по всей цепочке прототипов. Так же, как в классовых языках метод ищется сначала в объявлении класса, а затем - в объявлении родительского класса, а затем - в родительском классе родительского класса и так далее.

Но Руби и Яваскрипт не принципиально отличаются. У Руби тоже можно редактировать собственные свойства объекта и даже его "прототип" (когда пишут class << object и затем задают изменения). У Яваскрипта есть возможность узнать принадлежность объекта к классу через оператор instanceof: он проверяет, есть ли в цепочке прототипов объекта прототип этого класса. А современный Яваскрипт 6-й версии даже позволяет записывать объявление класса без всех этих Object.create(что-то-там.prototype).

Статья Википедии
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: AnnTenna, Kerotan, Rude, Mur

[RMMV] Пишем свой первый плагин 8 года 5 мес. назад #86761

  • lindar
  • lindar аватар
  • Вне сайта
  • Оседлый
  • Сообщений: 27
  • Спасибо получено: 3
Подскажите, а что и куда нужно добавить чтобы окно отображалось и на экране битвы?
Администратор запретил публиковать записи гостям.

[RMMV] Пишем свой первый плагин 8 года 5 мес. назад #86769

  • Mur
  • Mur аватар
  • Вне сайта
  • Светлый дракон
  • Мур? Мур! Мур.
  • Сообщений: 574
  • Спасибо получено: 1448
  • 2 место ОраторПрограммист JavaScript УчительОрганизатор конкурсовДаритель Стимкея
lindar пишет:
Подскажите, а что и куда нужно добавить чтобы окно отображалось и на экране битвы?

Уж не знаю зачем вам это понадобилось :unsure:

Но достаточно перед строчкой «function ItemOnMap() {»

Добавить, по аналогии с «_Scene_Map_createAllWindows»:
    //Обновление окон во время битвы
    var _Scene_Battle_createAllWindows = Scene_Battle.prototype.createAllWindows;
    Scene_Battle.prototype.createAllWindows = function() {
        _Scene_Battle_createAllWindows.call(this);
        this._itemOnMap = new ItemOnMap(10,10, 120, 60);
        this.addWindow(this._itemOnMap);
    };

Правда появятся проблемы:

ItemOnMap.png


Наше окошко будет перекрывать системные сообщения о битве. Поэтому нужно указать где-то другое место.

Буду делать в виде отдельного скрипта добавлю в настройки: Отображать ли окно во время битвы и координаты на экране.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: lindar, sondju

[RMMV] Пишем свой первый плагин 8 года 5 мес. назад #86770

  • lindar
  • lindar аватар
  • Вне сайта
  • Оседлый
  • Сообщений: 27
  • Спасибо получено: 3
Спасибо) перекрытие мелочи! Мне исключительно с целью отслеживания переменных)
Администратор запретил публиковать записи гостям.

[RMMV] Пишем свой первый плагин 8 года 5 мес. назад #86771

  • Mur
  • Mur аватар
  • Вне сайта
  • Светлый дракон
  • Мур? Мур! Мур.
  • Сообщений: 574
  • Спасибо получено: 1448
  • 2 место ОраторПрограммист JavaScript УчительОрганизатор конкурсовДаритель Стимкея
lindar пишет:
Мне исключительно с целью отслеживания переменных)

Хм, ну да, F9 во время битвы не работает,… :blush:
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: lindar

[RMMV] Пишем свой первый плагин 6 года 5 мес. назад #100316

  • Mur
  • Mur аватар
  • Вне сайта
  • Светлый дракон
  • Мур? Мур! Мур.
  • Сообщений: 574
  • Спасибо получено: 1448
  • 2 место ОраторПрограммист JavaScript УчительОрганизатор конкурсовДаритель Стимкея
Хех, ну что ж апнут тоже свою тему в тему конкурсу, тем более что вроде как Кусь изъявила желание попробовать себя в роли плагиностроителя. :blush:

Мдааа,… перечитываю сейчас и понимаю, как же все по детски написано *facepalm* ну с чего-то начинать надо ж было :silly:
Последнее редактирование: 6 года 5 мес. назад от Mur.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: DK, Cabbit

[RMMV] Пишем свой первый плагин 6 года 5 мес. назад #100317

  • DK
  • DK аватар
  • Вне сайта
  • Светлый дракон
  • DKPlugins
  • Сообщений: 946
  • Спасибо получено: 1129
  • ПаладинПрограммист JavaScript Проект месяца 2 местоОраторПроект месяца 3 местоРазработчикУчительПрограммист Ruby2 место Ветеран
Было бы хуже, если бы ты посчитала, что все нормально. А если ты видишь, что можно было написать лучше, значит ты улучшила свои навыки :)
Администратор запретил публиковать записи гостям.

[RMMV] Пишем свой первый плагин 6 года 5 мес. назад #100331

  • Mur
  • Mur аватар
  • Вне сайта
  • Светлый дракон
  • Мур? Мур! Мур.
  • Сообщений: 574
  • Спасибо получено: 1448
  • 2 место ОраторПрограммист JavaScript УчительОрганизатор конкурсовДаритель Стимкея
DK пишет:
Было бы хуже, если бы ты посчитала, что все нормально. А если ты видишь, что можно было написать лучше, значит ты улучшила свои навыки :)

Ну это понятно :) Пока мне интересно, я изучаю что-тоновое и развиваюсь. Ну а если мои мысли помогут ещё кому-то то только плюс. :blush:

Собственно осознание и переработка вылились в обновлённую статью. :woohoo:
Администратор запретил публиковать записи гостям.
Время создания страницы: 0.373 секунд