JS Работа дровосека

Всем привет, на работке в свободное время решил написать небольшую работу для этого замечательного ресурса. Может кому будет интересно, плюс если кто сможет, небольшое код-ревью дать, буду рад критике и советам.
Работа на чистом nodeJs + vue.

Начнём с серверной части, "рассадим" деревья по точкам:
Создадим массив с id-идентификатором дерева(понадобится в дальнейшем), и координатами(место "посадки" дерева)
JavaScript:
const allTrees = [
  {
    id: 1,
    x: 8.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
  {
    id: 2,
    x: 48.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
  {
    id: 3,
    x: 38.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
  {
    id: 4,
    x: 28.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
  {
    id: 5,
    x: 18.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
];
Теперь деревья нужно "рассадить", логичнее всего это делать при загрузке сервера, поэтому используем событие "packagesLoaded":
JavaScript:
mp.events.add("packagesLoaded", () => {
  console.log(`[INFO] Работа дровосека загружена`);
  allTrees.forEach((tree) => {
    const trees = mp.objects.new(
      "prop_tree_fallen_pine_01",
      new mp.Vector3(tree.x, tree.y, tree.z - 1),
      {
        dimension: 0,
      }
    );
    trees.treeId = tree.id;
    trees.setVariable("treeId", trees.treeId);
    trees.treesLabel = mp.labels.new(
      "Нажмите [Е]",
      new mp.Vector3(tree.x, tree.y + 1.1, tree.z),
      {
        los: false,
        font: 0,
        drawDistance: 20,
        color: [70, 130, 180, 170],
        dimension: 0,
      }
    );
  });
  console.log(`[INFO] Деревьев посажено: ${allTrees.length}`);
});
Для установки деревьев выбрал метод перебора созданного раннее массива allTrees - forEach, в котором создаём дерево для каждого объекта в массиве, а также назначаем каждому дереву собственный id и задаём собственный вариабл с этим же id для его использования уже на клиентской части.
Для визуального восприятия добавим надпись для каждого дерева "Нажмите [Е]".
Ну и немного работы с консолью в конце.
Получилось прекрасно..

1654421270290.png


Далее, чтобы начать взаимодействовать с этими деревьями нужно перейти в клиентскую часть.
Чтобы устроиться на работу, нужно для начала найти компанию(в данном случае npc), которая даст вам эту самую работу.
Создадим массив обектов(в данном случае, он один), зададим в нём координаты npc, его направление(heading), имя и модель
Почему массив объектов, потому что в дальнейшем можно будет с лёгкостью добавить ещё одного npc.
JavaScript:
const lumberjackNpc = [
  {
    x: 18.32457160949707,
    y: 26.563514709472656,
    z: 70.61154174804688,
    heading: "159.72958374023438",
    name: "Логан",
    model: "a_m_m_salton_02",
  },
];
Далее расставим npc всё тем же методом forEach, добавим под него колшейп, дадим класс этому колшейпу, чтобы в дальнейшем определять, что мы стоим перед этим самым npc, повесим над ним пару надписей и добавим блип на карту:
JavaScript:
lumberjackNpc.forEach((lumberjack) => {
  const lumberjackBot = mp.peds.new(
    mp.game.joaat(lumberjack.model),
    new mp.Vector3(lumberjack.x, lumberjack.y, lumberjack.z),
    lumberjack.heading,
    0
  );
  const lumberjackStartColshape = mp.colshapes.newSphere(
    parseFloat(lumberjack.x),
    parseFloat(lumberjack.y),
    parseFloat(lumberjack.z),
    2,
    0
  );
  lumberjackStartColshape.class = "lumberjack";
  mp.labels.new(
    lumberjack.name,
    {
      x: lumberjackBot.position.x,
      y: lumberjackBot.position.y,
      z: lumberjackBot.position.z + 0.9,
    },
    { los: true, font: 0, drawDistance: 20, color: [70, 130, 180, 170] }
  );
  mp.labels.new(
    "[Работа: Лесоруб]",
    {
      x: lumberjackBot.position.x,
      y: lumberjackBot.position.y,
      z: lumberjackBot.position.z + 1.2,
    },
    { los: true, font: 0, drawDistance: 20, color: [255, 255, 255, 170] }
  );
  const lumberjackBlip = mp.blips.new(
    792,
    new mp.Vector3(lumberjackBot.position.x, lumberjackBot.position.y, 0),
    {
      name: "Лесоруб",
      scale: 0.7,
      color: 5,
      shortRange: true,
      dimension: 0,
    }
  );
});
Вот он... Красавчик стоит, в шлёпках да в футболке, заправленной в трусы, а на футболке той топор нарисован, значит лесоруб, верно?

1654421964019.png
Теперь для того. чтобы взаимодействовать с данным npc, нужно использовать метод playerEnterColshape, также потребуется метод бинда кнопки mp.keys.bind, при нажатии на которую начнётся то самое взаимодействие.
JavaScript:
mp.events.add("playerEnterColshape", (shape) => {
  if (shape.class == "lumberjack") { // проверяем, на том ли мы колшейпе
    mp.keys.bind(0x45, true, openLumberjack); // биндим кнопку
  }
});
Также потребуется отвязывать кнопку Е, сделаем это методом playerExitColshape
JavaScript:
mp.events.add("playerExitColshape", (shape) => {
  if (shape.class == "lumberjack") {
    mp.keys.unbind(0x45, true, openLumberjack);
  }
});

Сама функция, отвечающая за показ диалога с Логаном:
JavaScript:
function openLumberjack() {
  if (lumberjackOpened) return;
  browser.execute("windows.lumberjack.lumberjackOpened = true");
  mp.gui.cursor.show(true, true);
  lumberjackOpened = true;
  mp.players.local.freezePosition(true);
}
Для того, чтобы проверять открыт ли диалог с данным npc или нет, создадим переменную lumberjackOpened и назначим ей значение сразу false (мы же не хотим по пустякам вызывать CEF на клиенте?)
JavaScript:
let lumberjackOpened = false;

По быстренькому накидаем страничку с диалогом:
HTML:
<template>
  <div v-if="lumberjackOpened" id="lumberjack" class="lumberjack">
    <h1>Логан</h1>
    <p>
      Приветствую, для работы потребуется только топор и немного терпения,
      дерзай!
    </p>
    <div class="buttons">
      <button v-if="!lumberjackStatus" @click="startLumberjack">
        Принять работу
      </button>
      <button v-if="lumberjackStatus" @click="stopLumberjack">Уволиться</button>
      <button @click="buyHatchet">Купить топор</button>
      <button @click="sell">Продать брёвна</button>
      <button @click="close">Закрыть</button>
    </div>
  </div>
</template>
<script src="./js/Lumberjack.js"></script>
<style src="./css/Lumberjack.css"></style>

css:
CSS:
.lumberjack {
    display: block;
    width: 700px;
    height: 600px;
    box-shadow: 1px black;
    border: none;
    margin: auto;
    margin-top: 100px;
    border-radius: 5px;
    background-color: rgba(0, 0, 0, 0.512);
    text-align: center;
    color: white;
    font-size: 1.3em;
    font-family: 'Courier New', Courier, monospace;
  }
  .lumberjack p {
      border-top: 1px solid white;
      border-bottom: 1px solid white;
      padding: 10px;
      background: #00000085;
  }
  .buttons {
    display: inline-grid;
  }
  .buttons button {
    margin: auto;
    margin-top:5px;
    width: 150px;
    height: 40px;
    border-radius: 5px;
    border: none;
    transition: 0.2s;
    background-color:rgba(20, 20, 20, 0.547);
    color: white;
  }
  .buttons button:hover{
  background-color:grey;
  }

js
JavaScript:
export default {
  name: "Lumberjack",
  data() {
    return {
      lumberjackOpened: false,
      lumberjackStatus: false
    };
  },
  methods: {
    sell() {
      window.mp.trigger("sellLogs");
    },
    stopLumberjack() {
      window.mp.trigger("stopLumberjack");
    },
    startLumberjack() {
      window.mp.trigger("startLumberjack");
    },
    buyHatchet() {
      window.mp.trigger("buyHatchet");
    },
    close() {
      this.lumberjackOpened = false;
      window.mp.trigger("closeLumberjack");
    }
  }
};
Теперь при нажатии на кнопку Е, рядом с npc появится небольшое простое окно:
1654423412339.png

Готово, осталось прописать события, которые мы вызываем при нажатии кнопок.
Закрыть:
JavaScript:
mp.events.add("closeLumberjack", () => {
  closeLumberjack(); // вызываем функцию закрытия окна
});

function closeLumberjack() { // сама функция
  lumberjackOpened = false;
  mp.gui.cursor.show(false, false);
  mp.players.local.freezePosition(false);
}

Принять работу, тут уже потребуется взаимодействие с серверной частью:
Код:
mp.events.add("startLumberjack", () => {
  mp.events.callRemote("server:startLumberjack"); // вызываем событие на сервере
});
Перейдём обратно в серверную часть и пропишем событие server:startLumberjack.
JavaScript:
mp.events.add("server:startLumberjack", (player) => {
  if (player.worker) return player.notify("Вы уже начали работу лесоруба"); // простая проверка на то, работает ли уже персонаж
  player.worker = true; // даём персонажу свойство, по которому определять, работает ли персонаж или нет
  player.notify("Вы устроились лесорубом"); // оповещаем персонажа
  player.setVariable("lumberjack", true); // вешаем на персонажа вариабл, чтобы использовать его на клиенте, для проверки, дровосек ли персонаж
  player.call("lumberJackStatus", [true]); // вызов эвента, который отключает кнопку "Принять работу" и включает кнопку "Уволиться" в диалоге с Логаном
});
То самое событие lumberJackStatus
JavaScript:
mp.events.add("lumberJackStatus", (status) => { // прилетает с сервера status = true или false
  if (status) { // если статус true
    browser.execute(`windows.lumberjack.lumberjackStatus = true`); // прячем кнопку "Принять работу" и показываем "Уволиться"
    setTimeout(() => {
      mp.keys.bind(0x45, true, useHatch); // биндим кнопку Е, для рубки дерева (не придумал ничего лучше, как биндить кнопку после 5 секунд, чтобы успеть отойти от npc, т.к при выходе из колшейпа, отвязывается кнопка Е, может кто подскажет решение получше :D 
    }, 5000);
  } else { // если статус false
    browser.execute(`windows.lumberjack.lumberjackStatus = false`); // прячем кнопку "Уволиться" и показываем "Принять работу"
    mp.keys.unbind(0x45, true, useHatch); // отвязываем кнопку Е
  }
});
Для того, чтобы понимать, где дерево и когда можно использовать кнопку Е для рубки, нужно научиться определять, что перед персонажем стоит нужный нам объект(а дерево - это объект). Для этого используем событие render и метод testCapsule.
JavaScript:
mp.events.add("render", () => {
  if (mp.players.local.getVariable("lumberjack")) {
    const startPosition = mp.players.local.getBoneCoords(12844, 0, 0, 0); // используем голову персонажа как начало "отрезка" поиска
    const endPosition = mp.players.local.getBoneCoords(12844, 0, 1, 0); // конец отрезка примерно на 1 метр, от головы персонажа
    const hitData = mp.raycasting.testCapsule( // "рисуем" невидимый отрезок
      startPosition,
      endPosition,
      0.5,
      mp.players.local
    );
    if (hitData && hitData.entity.type == "object") { // определяем, что отрезок попал на объект
      if (hitData.entity.getVariable("treeId")) { // определяем по заданному ранее вариаблу, что у объекта есть treeId, а значит это дерево
        mp.players.local.hatchReady = true; // задаём персонажу свойство, обозначающее, что он готов рубить дерево
        mp.players.local.treeFor = hitData.entity.getVariable("treeId"); // сохраняем id дерева, для его поиска на сервере
      }
    } else {
      mp.players.local.hatchReady = false; // если луч не коснулся объекта
    }
  }
});

Теперь пропишем функцию useHatch, которую мы биндили ранее:
JavaScript:
function useHatch() {
  if (mp.players.local.hatchReady && mp.players.local.treeFor) { // проверяем свойства, прописанные ранее
    mp.events.callRemote("server:startHatching", mp.players.local.treeFor); // вызываем событие на сервере для рубки, вместе с id дерева
  }
}
Перейдём на серверную часть и пропишем событие server:startHatching
JavaScript:
mp.events.add("server:startHatching", (player, id) => {
  if (!player.worker) return player.notify("Вы не устроились на работу"); // проверка на устроился ли персонаж
  if (!id) return; // ещё одна проверка, вдруг id затерялся
  if (!player.hasHatchet) return player.notify("У вас нет топора"); // проверка на куплен ли топор, вернёмся к этому позже
  player.playAnimation( // проигрывание анимации(да, я использовал анимацию ножа.., но самый терпеливый найдёт анимацию топора)
    "melee@knife@streamed_core",
    "knife_short_range_0",
    1,
    49
  );
  player.call("animActive", [true]); // вызываем событие, которое блокирует движение во время анимации
  setTimeout(() => { // таймаут на 3 с, после которого выполнятся команды ниже
    if (player) { // если игрок всё ещё на сервере, защита от вылета краша сервера
      player.logs += 1; // засчитываем + 1 бревно персонажу
      player.outputChatBox(`У вас ${player.logs} брёвен`); // вывод в чат кол-во брёвен
      player.stopAnimation(); // остановка анимации
      player.call("animActive", [false]); // вызываем событие, которое разрешает движение персонажу
    }
    mp.objects.forEach((object) => { // проходимся по массиву с созданными объектами
      if (object.getVariable("treeId") == id) { // ищем по вариабле, установленной ранее, объект с id, который прилетел с клиента
        object.treesLabel.destroy(); // уничтожаем надпись
        object.destroy(); // уничтожаем сам объект(дерево)
      }
    });
  }, 3000);
  setTimeout(() => { // таймаут на повторную "посадку" дерева
    const fi = allTrees.findIndex((tree) => tree.id == id); // ищем индекс дерева в массиве, по полученному id
    const tr = mp.objects.new(
      "prop_tree_fallen_pine_01",
      new mp.Vector3(allTrees[fi].x, allTrees[fi].y, allTrees[fi].z - 1), // создаём дерево из массива объектов, используя найденный индекс выше
      {
        dimension: 0,
      }
    );
    tr.treeId = allTrees[fi].id; // всё тоже самое, как и при packagesLoaded
    tr.setVariable("treeId", tr.treeId);
    tr.treesLabel = mp.labels.new(
      "Нажмите [Е]",
      new mp.Vector3(allTrees[fi].x, allTrees[fi].y + 1.1, allTrees[fi].z),
      {
        los: false,
        font: 0,
        drawDistance: 20,
        color: [70, 130, 180, 170],
        dimension: 0,
      }
    );
  }, 20000);
});
animActive на клиенте:
JavaScript:
mp.events.add("animActive", (data) => {
  if (data) {
    mp.players.local.freezePosition(true);
  } else {
    mp.players.local.freezePosition(false);
  }
});

Осталось лишь купить топор, и можно даже поработать! Для этого вернёмся на клиент и пропишем событие buyHatchet для созданной ранее кнопки
JavaScript:
mp.events.add("buyHatchet", () => {
  mp.events.callRemote("server:buyHatchet");
});
Перейдём обратно на сервер и пропишем там вызываемое событие:
но для начала пропишем фиксированную сумму за топор
JavaScript:
const hatchetPrice = 300;
JavaScript:
mp.events.add("server:buyHatchet", (player) => {
  if (player.hasHatchet) return player.notify("У вас уже есть топор"); // проверка на купленный топор
  if (player.money < hatchetPrice) // проверка на требуемое кол-во денег у персонажа
    return player.notify("У вас не хватает денег для покупки");
  player.giveWeapon(mp.joaat("weapon_hatchet"), 0); // выдача топора
  player.money -= hatchetPrice; // вычет суммы из денег персонажа за топор
  player.hasHatchet = true; // Свойство, определяющее наличие топора у персонажа
  player.notify("Вы купили топор"); // уведомление
});
Для того, чтобы задать свойства персонажу, такие как деньги и кол-во брёвен, используем событие playerReady
JavaScript:
mp.events.add("playerReady", (player) => {
  player.logs = 0;
  player.money = 300;
});
Отлично, топор можно купить, даже деревья им рубить, осталось только научиться продавать брёвна
Для этого мы возвращаемся на клиент и прописываем там событие sellLogs:
JavaScript:
mp.events.add("sellLogs", () => {
  mp.events.callRemote("server:sellLogs");
});
Переходим на сервер:
JavaScript:
mp.events.add("server:sellLogs", (player) => {
  if (player.logs <= 0) return player.notify("У вас нет бревён для продажи"); // проверка на брёвна
  const sum = player.logs * 300; // рассчитавыем сумму, которую дадим за n-ое кол-во брёвен
  player.money += sum; // плюсуем полученную сумму к уже имеющимся деньгам
  player.outputChatBox(
    `Вы продали ${player.logs} брёвен за ${sum}$. У вас ${player.money}$` // выводим сообщение в чат
  );
  player.logs = 0; // очищаем кол-во брёвен, ведь мы их продали
});
Вот и всё, наработались, теперь осталось уволиться
Для этого опять возвращаемся на клиент и прописываем событие stopLumberjack:
JavaScript:
mp.events.add("stopLumberjack", () => {
  mp.events.callRemote("server:stopLumberjack");
});

Сервер:
JavaScript:
mp.events.add("server:stopLumberjack", (player) => {
  if (player.worker) { // проверка на устройство
    player.worker = false; // свойства устройства на работу переходит в состояние false, что позволит ещё раз принять работу
    player.notify("Вы уволились"); // уведомление
    player.call("lumberJackStatus", [false]); // прячем, показываем кнопки
  } else {
    return player.notify("Вы не начали работу лесоруба"); // на всякий случай, если что-то пойдёт не так
  }
});
Вот и всё, вы уволены, можно пойти попить пивка
Работа готова, вроде всё на месте

Спасибо за внимание!
JavaScript:
let lumberjackOpened = false;
const lumberjackNpc = [
  {
    id: 0,
    x: 18.32457160949707,
    y: 26.563514709472656,
    z: 70.61154174804688,
    heading: "159.72958374023438",
    name: "Логан",
    model: "a_m_m_salton_02",
  },
];
lumberjackNpc.forEach((lumberjack) => {
  const lumberjackBot = mp.peds.new(
    mp.game.joaat(lumberjack.model),
    new mp.Vector3(lumberjack.x, lumberjack.y, lumberjack.z),
    lumberjack.heading,
    0
  );
  const lumberjackStartColshape = mp.colshapes.newSphere(
    parseFloat(lumberjack.x),
    parseFloat(lumberjack.y),
    parseFloat(lumberjack.z),
    2,
    0
  );
  lumberjackStartColshape.class = "lumberjack";
  mp.labels.new(
    lumberjack.name,
    {
      x: lumberjackBot.position.x,
      y: lumberjackBot.position.y,
      z: lumberjackBot.position.z + 0.9,
    },
    { los: true, font: 0, drawDistance: 20, color: [70, 130, 180, 170] }
  );
  mp.labels.new(
    "[Работа: Лесоруб]",
    {
      x: lumberjackBot.position.x,
      y: lumberjackBot.position.y,
      z: lumberjackBot.position.z + 1.2,
    },
    { los: true, font: 0, drawDistance: 20, color: [255, 255, 255, 170] }
  );
  const lumberjackBlip = mp.blips.new(
    792,
    new mp.Vector3(lumberjackBot.position.x, lumberjackBot.position.y, 0),
    {
      name: "Лесоруб",
      scale: 0.7,
      color: 5,
      shortRange: true,
      dimension: 0,
    }
  );
});
mp.events.add("sellLogs", () => {
  mp.events.callRemote("server:sellLogs");
});
mp.events.add("startLumberjack", () => {
  mp.events.callRemote("server:startLumberjack");
});
mp.events.add("stopLumberjack", () => {
  mp.events.callRemote("server:stopLumberjack");
});
mp.events.add("buyHatchet", () => {
  mp.events.callRemote("server:buyHatchet");
});
mp.events.add("closeLumberjack", () => {
  closeLumberjack();
});
mp.events.add("playerEnterColshape", (shape) => {
  if (shape.class == "lumberjack") {
    mp.keys.bind(0x45, true, openLumberjack);
  }
});
mp.events.add("playerExitColshape", (shape) => {
  if (shape.class == "lumberjack") {
    mp.keys.unbind(0x45, true, openLumberjack);
  }
});
mp.events.add("lumberJackStatus", (status) => {
  if (status) {
    browser.execute(`windows.lumberjack.lumberjackStatus = true`);
    setTimeout(() => {
      mp.keys.bind(0x45, true, useHatch);
    }, 5000);
  } else {
    browser.execute(`windows.lumberjack.lumberjackStatus = false`);
    mp.keys.unbind(0x45, true, useHatch);
  }
});
mp.events.add("animActive", (data) => {
  if (data) {
    mp.players.local.freezePosition(true);
  } else {
    mp.players.local.freezePosition(false);
  }
});
mp.events.add("render", () => {
  if (mp.players.local.getVariable("lumberjack")) {
    const startPosition = mp.players.local.getBoneCoords(12844, 0, 0, 0);
    const endPosition = mp.players.local.getBoneCoords(12844, 0, 1, 0);
    const hitData = mp.raycasting.testCapsule(
      startPosition,
      endPosition,
      0.5,
      mp.players.local
    );
    if (hitData && hitData.entity.type == "object") {
      if (hitData.entity.getVariable("treeId")) {
        mp.players.local.hatchReady = true;
        mp.players.local.treeFor = hitData.entity.getVariable("treeId");
      }
    } else {
      mp.players.local.hatchReady = false;
    }
  }
});
function closeLumberjack() {
  lumberjackOpened = false;
  mp.gui.cursor.show(false, false);
  mp.players.local.freezePosition(false);
}
function openLumberjack() {
  if (lumberjackOpened) return;
  browser.execute("windows.lumberjack.lumberjackOpened = true");
  mp.gui.cursor.show(true, true);
  lumberjackOpened = true;
  mp.players.local.freezePosition(true);
}
function useHatch() {
  if (mp.players.local.hatchReady && mp.players.local.treeFor) {
    mp.events.callRemote("server:startHatching", mp.players.local.treeFor);
  }
}
JavaScript:
const hatchetPrice = 300;
const allTrees = [
  {
    id: 1,
    x: 8.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
  {
    id: 2,
    x: 48.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
  {
    id: 3,
    x: 38.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
  {
    id: 4,
    x: 28.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
  {
    id: 5,
    x: 18.031051635742188,
    y: 10.031051635742188,
    z: 70.78174591064453,
  },
];
mp.events.add("packagesLoaded", () => {
  console.log(`[INFO] Работа дровосека загружена`);
  allTrees.forEach((tree) => {
    const trees = mp.objects.new(
      "prop_tree_fallen_pine_01",
      new mp.Vector3(tree.x, tree.y, tree.z - 1),
      {
        dimension: 0,
      }
    );
    trees.treeId = tree.id;
    trees.setVariable("treeId", trees.treeId);
    trees.treesLabel = mp.labels.new(
      "Нажмите [Е]",
      new mp.Vector3(tree.x, tree.y + 1.1, tree.z),
      {
        los: false,
        font: 0,
        drawDistance: 20,
        color: [70, 130, 180, 170],
        dimension: 0,
      }
    );
  });
  console.log(`[INFO] Деревьев посажено: ${allTrees.length}`);
});
mp.events.add("playerReady", (player) => {
  player.logs = 0;
  player.money = 300;
});
mp.events.add("server:buyHatchet", (player) => {
  if (player.hasHatchet) return player.notify("У вас уже есть топор");
  if (player.money < hatchetPrice)
    return player.notify("У вас не хватает денег для покупки");
  player.giveWeapon(mp.joaat("weapon_hatchet"), 0);
  player.money -= hatchetPrice;
  player.hasHatchet = true;
  player.notify("Вы купили топор");
});
mp.events.add("server:sellLogs", (player) => {
  if (player.logs <= 0) return player.notify("У вас нет бревён для продажи");
  const sum = player.logs * 300;
  player.money += sum;
  player.outputChatBox(
    `Вы продали ${player.logs} брёвен за ${sum}$. У вас ${player.money}$`
  );
  player.logs = 0;
});
mp.events.add("server:startHatching", (player, id) => {
  if (!player.worker) return player.notify("Вы не устроились на работу");
  if (!id) return;
  if (!player.hasHatchet) return player.notify("У вас нет топора");
  player.playAnimation(
    "melee@knife@streamed_core",
    "knife_short_range_0",
    1,
    49
  );
  player.call("animActive", [true]);
  setTimeout(() => {
    if (player) {
      player.logs += 1;
      player.outputChatBox(`У вас ${player.logs} брёвен`);
      player.stopAnimation();
      player.call("animActive", [false]);
    }
    mp.objects.forEach((object) => {
      if (object.getVariable("treeId") == id) {
        object.treesLabel.destroy();
        object.destroy();
      }
    });
  }, 3000);
  setTimeout(() => {
    const fi = allTrees.findIndex((tree) => tree.id == id);
    const tr = mp.objects.new(
      "prop_tree_fallen_pine_01",
      new mp.Vector3(allTrees[fi].x, allTrees[fi].y, allTrees[fi].z - 1),
      {
        dimension: 0,
      }
    );
    tr.treeId = allTrees[fi].id;
    tr.setVariable("treeId", tr.treeId);
    tr.treesLabel = mp.labels.new(
      "Нажмите [Е]",
      new mp.Vector3(allTrees[fi].x, allTrees[fi].y + 1.1, allTrees[fi].z),
      {
        los: false,
        font: 0,
        drawDistance: 20,
        color: [70, 130, 180, 170],
        dimension: 0,
      }
    );
  }, 20000);
});
mp.events.add("server:startLumberjack", (player) => {
  if (player.worker) return player.notify("Вы уже начали работу лесоруба");
  player.worker = true;
  player.notify("Вы устроились лесорубом");
  player.setVariable("lumberjack", true);
  player.call("lumberJackStatus", [true]);
});
mp.events.add("server:stopLumberjack", (player) => {
  if (player.worker) {
    player.worker = false;
    player.notify("Вы уволились");
    player.call("lumberJackStatus", [false]);
  } else {
    return player.notify("Вы не начали работу лесоруба");
  }
});
 

Lev Angel

Developer
Команда форума
Скриптер
Сообщения
795
Ого, круто! Спасибо за такой детальный туториал! По коду гляну чуть позже и дам детальный фидбек.
 

Lev Angel

Developer
Команда форума
Скриптер
Сообщения
795
Респект за полноценную работу(y) С небольшими доработками это можно использовать в своем моде. Да и расписано все подробно, что можно будет разобраться что и где.

Теперь перейдем к код ревью. Все ниже описанное не критика. А просто предложения как можно улучшить код, повысить читаемость или надежность;)

1. Координаты деревьев лучше хранить в отдельном json файле и просто подгружать их. Таких координат могут быть сотни и они будут только загромождать сам скрипт.

2. dimension: 0, Значения по умолчанию можно не писать, чтобы упростить код

3. Magic number detected
JavaScript:
mp.keys.bind(0x45, true, openLumberjack); // биндим кнопку
код кнопки лучше вверху записать в понятную константу и везде ее юзать. Потом если понадобиться ее поменять, то магическое число можно и пропустить.

4. Про определение что перед игроком дерево - респект. Просто и понятно. Если развить эту идею, то можно так в теории определять любое дерево на сервере по хэшу модели.

5. В "server:startHatching" нужно наверно еще проверять чтобы топор был в руках. Тогда возможно нет смысла в player.hasHatchet, а полагаться только на то есть ли он в руках или нет.

6.
JavaScript:
if (player) { // если игрок всё ещё на сервере, защита от вылета краша сервера
Вот тут не очень безопасно. Сам объект может остаться и словим ошибку "Expired multiplayer object has been used". Лучше валидность игрока проверять через mp.players.exists(player)

7.
JavaScript:
mp.events.add("server:startHatching", (player, id) => {

});

Слишком большой ивент в котором много чего происходит. Как минимум содержимое таймаутов я бы вынес в отдельные функции. Это упростит сам ивент и будет очевидно что происходит в таймерах.

8. В таймере на повторную посадку деревьев идет дублирование кода. Нужно вынести функционал связанный с добавлением дерева в отдельную функцию и переиспользовать ее в таймере и при старте сервера когда садим все деревья.

9.
JavaScript:
 const sum = player.logs * 300;
Цену за одно бревно лучше вынести в конфиг или константу в начале скрипта. Будет проще его менять, когда будет необходимость и минус одно магическое число в коде.

Именования

1.
JavaScript:
const trees = mp.objects.new(
Множественное число, для одного объекта может вводить в заблуждение. Мб как-то treeObject

2.
JavaScript:
browser.execute("windows.lumberjack.lumberjackOpened = true");
без дублирования наименования имхо красивее
JavaScript:
browser.execute("windows.lumberjack.opened = true");

3. player.logs традиционно это больше напоминает именно логи. И в большом моде, над которым работают разные люди может привести к конфликту и недопониманию. Для бревна есть и другие варианты перевода.
Возможно неплохая практика хранить все что касается какой-то работы или модуля в отдельном namespace, типо player.lumberjack.hasHatchet
player.lumberjack.logs
 

Verdiji

Junior Developer
Скриптер
Сообщения
30
Спасибо большое! Все предложения изучены и будут учтены в дальнейшем, по поводу player.logs - да, тоже думал про логи, но т.к "система" сейчас выставлена как отдельный "блок", подумал, что нормас))
Дальше люди уже смогут использовать свою фантазию и добавлять самостоятельно какие-то нюансы, по типу, максимум брёвен на персонаже, отвозить брёвна, делать из стоящих деревьев после "рубки" лежачие и т.д)
 

Uristri

Trainee
Сообщения
12
Не слушай Лева по поводу дименшона, он в своём рейдж билдере не прописывал дименш а я потратил несколько часов чтобы допереть в чём проблема. :D
 

Lev Angel

Developer
Команда форума
Скриптер
Сообщения
795
Все там четко ;) Стандартные значения нет смысла каждый раз прописывать.
 
Верх