aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord-bots

Discord Economy Bot: Munten, Dagbeloning en Winkel

Een Discord economy bot voegt een gamificatielaag toe aan je server die leden actief houdt: ze verdienen een virtuele munt, halen dagelijkse beloningen op en geven hun spaargeld uit in een winkel om rollen of items te kopen. In deze gids bouwen we vanaf nul een werkend economiesysteem met discord.js, met commando's voor saldo, dagbeloning en winkel. We gebruiken moderne slash commands en slaan de gegevens op in een persistente database.

Vereisten en projectopzet

Om te beginnen heb je Node.js 18 of nieuwer nodig, een code-editor en een botapplicatie aangemaakt in het Discord Developer Portal. De Message Content-intent is hier niet vereist omdat we slash commands gebruiken, maar bij het uitnodigen van de bot naar je server moet je de scopes applications.commands en bot selecteren.

Maak een nieuwe map aan en installeer de afhankelijkheden:

npm init -y
npm install discord.js better-sqlite3 dotenv

In plaats van de token en applicatie-ID hard in de code te zetten, bewaren we ze in een .env-bestand:

DISCORD_TOKEN=jouw_bot_token
CLIENT_ID=applicatie_id
GUILD_ID=test_server_id

Databaseschema

De economiedata moet persistent zijn; saldo's mogen niet resetten elke keer dat de bot opnieuw start. We gebruiken better-sqlite3 omdat het triviaal op te zetten is. Eén tabel is voldoende:

const Database = require('better-sqlite3');
const db = new Database('economy.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    user_id    TEXT PRIMARY KEY,
    balance    INTEGER NOT NULL DEFAULT 0,
    last_daily INTEGER
  );
`);

function getUser(id) {
  let row = db.prepare('SELECT * FROM users WHERE user_id = ?').get(id);
  if (!row) {
    db.prepare('INSERT INTO users (user_id) VALUES (?)').run(id);
    row = { user_id: id, balance: 0, last_daily: null };
  }
  return row;
}

function addBalance(id, amount) {
  getUser(id);
  db.prepare('UPDATE users SET balance = balance + ? WHERE user_id = ?').run(amount, id);
}

Het veld last_daily slaat als Unix-tijdstempel op wanneer de dagbeloning voor het laatst is opgehaald. Elke muntmutatie via één enkele addBalance-functie laten lopen maakt het later veel makkelijker om logging of transactiebeveiliging toe te voegen.

Commando's definiëren en registreren

In recente versies van discord.js moeten slash commands apart bij de Discord-API worden geregistreerd. Tijdens de ontwikkeling wordt registratie bij één server direct bijgewerkt; globale registratie kan tot een uur duren om door te komen. We schrijven een apart deploy-commands.js-script:

const { REST, Routes, SlashCommandBuilder } = require('discord.js');
require('dotenv').config();

const commands = [
  new SlashCommandBuilder().setName('balance').setDescription('Toon je saldo'),
  new SlashCommandBuilder().setName('daily').setDescription('Haal je dagbeloning op'),
  new SlashCommandBuilder().setName('shop').setDescription('Toon de winkel'),
  new SlashCommandBuilder()
    .setName('buy')
    .setDescription('Koop een item uit de winkel')
    .addStringOption(o =>
      o.setName('item').setDescription('Itemsleutel').setRequired(true)),
].map(c => c.toJSON());

const rest = new REST().setToken(process.env.DISCORD_TOKEN);
rest.put(
  Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
  { body: commands }
).then(() => console.log('Commando\'s geregistreerd.'));

Logica voor saldo en dagbeloning

Nu verwerken we de interacties in het hoofdbestand van de bot. Het cruciale deel van de dagbeloning is de cooldown-controle die een gebruiker beperkt tot één keer ophalen per 24 uur. Dat doen we door te vergelijken met last_daily:

const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const DAILY_AMOUNT = 250;
const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; // 24 uur (ms)

client.on('interactionCreate', async (i) => {
  if (!i.isChatInputCommand()) return;
  const user = getUser(i.user.id);

  if (i.commandName === 'balance') {
    await i.reply(`💰 Je saldo: **${user.balance}** munten.`);
  }

  if (i.commandName === 'daily') {
    const now = Date.now();
    if (user.last_daily && now - user.last_daily < DAILY_COOLDOWN) {
      const resterend = user.last_daily + DAILY_COOLDOWN - now;
      const uren = Math.ceil(resterend / (60 * 60 * 1000));
      return i.reply(`⏳ Wacht ~${uren}u op je volgende beloning.`);
    }
    addBalance(i.user.id, DAILY_AMOUNT);
    db.prepare('UPDATE users SET last_daily = ? WHERE user_id = ?').run(now, i.user.id);
    await i.reply(`🎁 Je hebt ${DAILY_AMOUNT} munten verdiend!`);
  }
});

client.login(process.env.DISCORD_TOKEN);

Tijdstempels in milliseconden opslaan en vergelijken elimineert tijdzoneproblemen volledig. Een regel "24 uur sinds de laatste keer ophalen" is zowel eerlijker als robuuster te implementeren dan een systeem dat "om middernacht reset".

Winkel en aankoopflow

Voor de winkel volstaat het om items als een object in de code te definiëren; een aparte tabel is niet vereist op kleine servers. Elk item heeft een sleutel, een naam, een prijs en optioneel de ID van een Discord-rol die het verleent:

const SHOP = {
  vip:   { name: 'VIP-rol', price: 1000, roleId: 'ROL_ID' },
  kleur: { name: 'Gekleurde naam', price: 500, roleId: 'ROL_ID_2' },
};

In het koopcommando controleren we eerst het saldo, trekken de kosten af als het voldoende is en kennen de rol toe. De saldocontrole en de aftrek in één keer doen voorkomt een negatief saldo bij race-condities zoals dubbelklikken:

if (i.commandName === 'shop') {
  const lijst = Object.entries(SHOP)
    .map(([k, v]) => `• \`${k}\` — ${v.name}: ${v.price} munten`)
    .join('\n');
  return i.reply(`🛒 **Winkel**\n${lijst}`);
}

if (i.commandName === 'buy') {
  const key = i.options.getString('item');
  const item = SHOP[key];
  if (!item) return i.reply('❌ Dat item bestaat niet.');
  if (user.balance < item.price) return i.reply('❌ Niet genoeg munten.');

  addBalance(i.user.id, -item.price);
  if (item.roleId) {
    const member = await i.guild.members.fetch(i.user.id);
    await member.roles.add(item.roleId).catch(() => null);
  }
  await i.reply(`✅ **${item.name}** gekocht!`);
}

Om het toekennen van de rol te laten werken, moet de eigen rol van de bot hoger in de lijst staan dan de rol die hij toekent, en heeft de bot de permissie "Rollen beheren" nodig. Anders geeft Discord een permissiefout; die netjes afhandelen met .catch() zodat de gebruiker geïnformeerd wordt zonder dat de bot crasht, is een goede gewoonte.

Beveiliging en verbetertips

Het kernsysteem is klaar, maar een paar punten verdienen aandacht voordat je naar productie gaat. Als je een systeem toevoegt dat munten per bericht beloont, zet dan een korte cooldown per gebruiker om spam te voorkomen. Valideer saldo's altijd aan de serverkant en vertrouw nooit een waarde die van de client komt. Om de bot 24/7 online te houden, is de betrouwbaarste aanpak hem op een kleine VPS te draaien met een procesmanager zoals pm2.

Veelgestelde vragen

Kan ik MySQL als database gebruiken?

Ja. SQLite is ideaal vanwege het gemak van opzetten, maar voor een grote bot die op meerdere servers draait, past MySQL of PostgreSQL beter. De querylogica blijft hetzelfde; je wisselt alleen de databasedriver en de verbinding.

Waarom wordt de dagbeloning met een tijdstempel gecontroleerd?

Omdat "reset elke middernacht"-benaderingen beïnvloed worden door tijdzoneverschillen en gevoelig zijn voor misbruik. Het tijdstip van de laatste keer ophalen opslaan en controleren of er 24 uur zijn verstreken, is eerlijk en tijdzone-onafhankelijk.

Verdwijnt een gekochte rol als de bot offline gaat?

Nee. De rol wordt aan het lid toegekend aan de kant van Discord en is permanent; hij blijft zelfs als de bot uitvalt. Alleen het muntsaldo staat in de database, dus zorg dat je die niet kwijtraakt.

Wil je een professionele, schaalbare economy bot voor je server? Ik kan een bot op maat bouwen die past bij jouw wensen, met aangepaste commando's, seizoenen en panelintegratie. Om je project te bespreken, neem contact met me op.

Devamı için