<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tack k</title>
    <description>The latest articles on DEV Community by Tack k (@tackk3).</description>
    <link>https://dev.to/tackk3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3825540%2F79b947b0-030a-4f05-a397-2ed6c830bf3a.jpeg</url>
      <title>DEV Community: Tack k</title>
      <link>https://dev.to/tackk3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tackk3"/>
    <language>en</language>
    <item>
      <title>Two Police Scripts That Made Our QBCore RP Server Feel More Real</title>
      <dc:creator>Tack k</dc:creator>
      <pubDate>Sat, 28 Mar 2026 22:32:09 +0000</pubDate>
      <link>https://dev.to/tackk3/two-police-scripts-that-made-our-qbcore-rp-server-feel-more-real-5l</link>
      <guid>https://dev.to/tackk3/two-police-scripts-that-made-our-qbcore-rp-server-feel-more-real-5l</guid>
      <description>&lt;h2&gt;
  
  
  The gap between RP and reality
&lt;/h2&gt;

&lt;p&gt;In most QBCore servers, police work feels incomplete in two specific ways.&lt;/p&gt;

&lt;p&gt;First: when you arrest someone and search them, you can remove items — but there's no system for what happens to those items. They just disappear. No evidence bag, no chain of custody, no way to retrieve seized goods later.&lt;/p&gt;

&lt;p&gt;Second: communication between officers happens in voice chat or external Discord. There's no in-game system for issuing wanted notices, alerting other units when a major crime wraps up, or signaling that officers are available for large-scale operations.&lt;/p&gt;

&lt;p&gt;I built two scripts to fix both: &lt;strong&gt;gg_oushu&lt;/strong&gt; for evidence seizure, and &lt;strong&gt;gg_police_notice&lt;/strong&gt; for police communications.&lt;/p&gt;




&lt;h2&gt;
  
  
  gg_oushu — Evidence seizure and packaging
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The concept
&lt;/h3&gt;

&lt;p&gt;When a police officer uses the &lt;code&gt;/seize&lt;/code&gt; command near a suspect, the script scans that player's inventory for any items on the seizeable list — weapons, drugs, stolen goods, heist tools, and more. All matching items are removed from the suspect and bundled into a single &lt;code&gt;seized_package&lt;/code&gt; item in the officer's inventory.&lt;/p&gt;

&lt;p&gt;That package persists. It can be opened later, items can be extracted individually, and the whole thing is tracked in the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the package works
&lt;/h3&gt;

&lt;p&gt;Each package gets a unique key generated at seizure time. The items inside are stored in MySQL — not just in the inventory metadata. This matters because ox_inventory metadata can get stale when items are moved or dropped, but the database record stays accurate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pkg_"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="nb"&gt;os.time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;"_"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="nb"&gt;math.random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9999&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oxmysql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'INSERT INTO seized_packages (id, player, items) VALUES (?, ?, ?)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GetPlayerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seizedItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an officer opens the package, the server fetches the current state directly from the database — not from the cached metadata. This ensures what you see in the UI matches what's actually stored, even if the package has been partially emptied.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extracting items
&lt;/h3&gt;

&lt;p&gt;Inside the package UI, each seized item is listed individually. The officer can extract items one at a time. When an item is extracted, the database record is updated immediately. When the last item is extracted, the package item is removed from inventory and the database record is deleted.&lt;/p&gt;

&lt;p&gt;There's also a force-delete option for disposing of an entire package at once — useful for clearing evidence that doesn't need to be processed item by item.&lt;/p&gt;

&lt;h3&gt;
  
  
  Race safety
&lt;/h3&gt;

&lt;p&gt;Stock deduction and package deletion use database-level operations to prevent the same item from being extracted twice if two operations happen simultaneously. The server is the single source of truth — the client never decides what's in a package.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring seizeable items
&lt;/h3&gt;

&lt;p&gt;The list of seizeable items is defined in config — weapons, drugs, heist tools, stolen valuables. Easy to extend for any custom items on your server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeizableItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"weapon_pistol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"drug_meth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"drill"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"lockpick"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- add your server's custom items here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeizeDistance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="c1"&gt;-- max distance for seizure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  gg_police_notice — In-game police communications
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Three notification types
&lt;/h3&gt;

&lt;p&gt;The script handles three distinct communication scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wanted notices&lt;/strong&gt; — issue a formal wanted notice for a suspect, with name, charges, vehicle plate, and expiry time. Sends to Discord with formatting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closure alerts&lt;/strong&gt; — notify all relevant units when a major crime event ends. These go both to Discord and as an in-game overlay notification to every online player in a configured list of jobs (police, EMS, etc.).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large operation alerts&lt;/strong&gt; — signal that officers are available and ready for large-scale crime response.&lt;/p&gt;

&lt;h3&gt;
  
  
  The closure overlay
&lt;/h3&gt;

&lt;p&gt;The most interesting piece is the closure alert's in-game overlay. When a closure is sent, every player in the target job list sees an overlay appear on their screen — not just a text notification, but a visible UI element with a sound cue.&lt;/p&gt;

&lt;p&gt;The overlay appears without stealing focus (players can still control their character). If they want to interact with it, they press a configurable key to bring focus to the overlay, then close it manually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Server broadcasts to all target jobs&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QBCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPlayers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;Player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QBCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Player&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLOSURE_TARGET_JOBS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PlayerData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;TriggerClientEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"gg_police_notice:showClosureNUI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;crime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;selectedCrime&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Permission check
&lt;/h3&gt;

&lt;p&gt;All three notification types check server-side that the sender is a police officer before doing anything. Client-side restriction (hiding the menu from non-police) is for UX — the server-side check is what actually enforces it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;Player&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PlayerData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="s2"&gt;"police"&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Non-police attempted to send notice: "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;GetPlayerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Discord webhook configuration
&lt;/h3&gt;

&lt;p&gt;Each notification type has its own webhook URL. This lets you route wanted notices, closure alerts, and large operation signals to different Discord channels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Set your own webhook URLs in config&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WANTED_WEBHOOK&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLOSURE_WEBHOOK&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LARGEALERT_WEBHOOK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why these two scripts belong together
&lt;/h2&gt;

&lt;p&gt;Both scripts solve the same underlying problem: the gap between what real police work looks like and what most FiveM servers actually provide.&lt;/p&gt;

&lt;p&gt;Evidence seizure makes arrests feel meaningful — there's a record, a physical item, a process. Police notices make inter-unit communication feel real — wanted suspects get formal notices, major events get proper closure, large operations get coordinated.&lt;/p&gt;

&lt;p&gt;Neither script is technically complex. But both make the server feel more like a functioning world and less like a game with police-shaped NPCs.&lt;/p&gt;




&lt;p&gt;Built with Claude Opus as my coding partner. More scripts from 2+ years of server operation coming soon.&lt;/p&gt;

</description>
      <category>fivem</category>
      <category>qbcore</category>
      <category>lua</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>How I Built a Practical Reservation System from Scratch</title>
      <dc:creator>Tack k</dc:creator>
      <pubDate>Tue, 24 Mar 2026 03:08:54 +0000</pubDate>
      <link>https://dev.to/tackk3/how-i-built-a-practical-reservation-system-from-scratch-2d17</link>
      <guid>https://dev.to/tackk3/how-i-built-a-practical-reservation-system-from-scratch-2d17</guid>
      <description>&lt;h1&gt;
  
  
  How I Built a Practical Reservation System from Scratch
&lt;/h1&gt;

&lt;p&gt;When I started building my own reservation system, I didn’t aim to create something “perfect”.&lt;/p&gt;

&lt;p&gt;I just wanted something that actually works in real-world usage.&lt;/p&gt;

&lt;p&gt;In this article, I’ll share:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why I built it&lt;/li&gt;
&lt;li&gt;The problems I faced&lt;/li&gt;
&lt;li&gt;How I designed it&lt;/li&gt;
&lt;li&gt;What I would improve next&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're thinking about building your own booking system, this might save you a lot of time.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Why I Built a Reservation System
&lt;/h2&gt;

&lt;p&gt;I run a business where managing bookings is essential.&lt;/p&gt;

&lt;p&gt;But most existing tools had problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too complex&lt;/li&gt;
&lt;li&gt;Too expensive&lt;/li&gt;
&lt;li&gt;Not customizable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I decided:&lt;/p&gt;

&lt;p&gt;👉 “I’ll just build my own.”&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚠️ The Real Problems Behind Reservation Systems
&lt;/h2&gt;

&lt;p&gt;At first, it looks simple.&lt;/p&gt;

&lt;p&gt;“User selects time → done”&lt;/p&gt;

&lt;p&gt;But reality is different.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Double Booking
&lt;/h3&gt;

&lt;p&gt;This is the biggest issue.&lt;/p&gt;

&lt;p&gt;If two users book the same time slot simultaneously, you get a conflict.&lt;/p&gt;

&lt;p&gt;I handle this by validating availability before confirming reservations.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Time Slot Management
&lt;/h3&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Menu A → 60 minutes&lt;/li&gt;
&lt;li&gt;Menu B → 90 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means:&lt;br&gt;
👉 You can’t just treat time as fixed blocks&lt;/p&gt;

&lt;p&gt;You need dynamic slot calculation.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. UX Matters More Than You Think
&lt;/h3&gt;

&lt;p&gt;Users don’t care about your architecture.&lt;/p&gt;

&lt;p&gt;They want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast UI&lt;/li&gt;
&lt;li&gt;Clear availability&lt;/li&gt;
&lt;li&gt;No confusion&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏗️ My System Design Approach
&lt;/h2&gt;

&lt;p&gt;I kept it simple and practical.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔹 Structure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: Flutter&lt;/li&gt;
&lt;li&gt;Backend: PHP API&lt;/li&gt;
&lt;li&gt;Database: JSON (intentionally chosen for simplicity in early-stage development)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔹 Core Concept: Time Slot Calculation
&lt;/h3&gt;

&lt;p&gt;Instead of fixed slots:&lt;/p&gt;

&lt;p&gt;Store reservations → calculate availability dynamically&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;for each reservation:&lt;br&gt;
  block time range (start → end)&lt;/p&gt;

&lt;p&gt;generate available slots based on remaining gaps&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 Reservation Flow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;User selects date&lt;/li&gt;
&lt;li&gt;System calculates available slots&lt;/li&gt;
&lt;li&gt;User selects menu&lt;/li&gt;
&lt;li&gt;Duration is applied automatically&lt;/li&gt;
&lt;li&gt;Reservation is saved&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  💡 Key Features I Implemented
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✔️ Dynamic Duration Mapping
&lt;/h3&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;"menu": "Fade Cut"&lt;/p&gt;

&lt;p&gt;I map it like:&lt;/p&gt;

&lt;p&gt;"Fade Cut": 90&lt;/p&gt;

&lt;p&gt;👉 This allows automatic slot calculation.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✔️ Multi-slot Reservation Display
&lt;/h3&gt;

&lt;p&gt;If a reservation spans multiple slots:&lt;/p&gt;

&lt;p&gt;👉 Merge into a single block in UI&lt;/p&gt;

&lt;p&gt;This improves readability a lot.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✔️ Admin Mode
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Login system&lt;/li&gt;
&lt;li&gt;Editable schedule&lt;/li&gt;
&lt;li&gt;Mobile support&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚧 What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Simple &amp;gt; Perfect
&lt;/h3&gt;

&lt;p&gt;Trying to over-engineer early is a mistake.&lt;/p&gt;

&lt;p&gt;Start simple. Improve later.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Backend Logic Is Everything
&lt;/h3&gt;

&lt;p&gt;UI is just a viewer.&lt;/p&gt;

&lt;p&gt;The real power is:&lt;br&gt;
👉 how you calculate availability&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Reservation Systems Are NOT Easy
&lt;/h3&gt;

&lt;p&gt;It looks simple, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;concurrency&lt;/li&gt;
&lt;li&gt;UX&lt;/li&gt;
&lt;li&gt;business rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All combine into a complex system.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 What I Want to Improve Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Payment integration&lt;/li&gt;
&lt;li&gt;Notification system (LINE / Email)&lt;/li&gt;
&lt;li&gt;Continuous security improvements&lt;/li&gt;
&lt;li&gt;Database migration (maybe Laravel)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔚 Conclusion
&lt;/h2&gt;

&lt;p&gt;Building a reservation system taught me more than I expected.&lt;/p&gt;

&lt;p&gt;Not just coding — but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;business logic&lt;/li&gt;
&lt;li&gt;UX thinking&lt;/li&gt;
&lt;li&gt;system design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're thinking about building one:&lt;/p&gt;

&lt;p&gt;👉 Do it.&lt;/p&gt;

&lt;p&gt;You’ll learn a lot.&lt;/p&gt;




&lt;p&gt;If you have questions or want to build something similar, feel free to reach out 🙌&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>flutter</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>From ChatGPT to Claude Code — How My AI Workflow Evolved Over 2 Years of FiveM Dev</title>
      <dc:creator>Tack k</dc:creator>
      <pubDate>Mon, 23 Mar 2026 01:27:34 +0000</pubDate>
      <link>https://dev.to/tackk3/from-chatgpt-to-claude-code-how-my-ai-workflow-evolved-over-2-years-of-fivem-dev-25pj</link>
      <guid>https://dev.to/tackk3/from-chatgpt-to-claude-code-how-my-ai-workflow-evolved-over-2-years-of-fivem-dev-25pj</guid>
      <description>&lt;h2&gt;
  
  
  It started with a frustration
&lt;/h2&gt;

&lt;p&gt;Two years ago, I wanted to build custom scripts for my FiveM roleplay server. I knew exactly what I wanted — a custom billing system with quantity-based invoicing, a police reward distribution tool, an in-game arcade with real playable games, vending machines that could track stock and revenue by job.&lt;/p&gt;

&lt;p&gt;The vision was clear. The problem was execution.&lt;/p&gt;

&lt;p&gt;I don't write Lua. I don't write JavaScript. I'm a systems designer — I think in data flows, user interactions, and edge cases. The "how it should work" part comes naturally. The "typing the actual code" part does not.&lt;/p&gt;

&lt;p&gt;So I turned to AI. And what followed was two years of learning what these tools could and couldn't do — and watching that ceiling get higher every few months.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1 — ChatGPT (2 years ago)
&lt;/h2&gt;

&lt;p&gt;ChatGPT was the obvious starting point. Everyone was talking about it. I figured if it could write essays and answer questions, it could write Lua.&lt;/p&gt;

&lt;p&gt;It could. Sort of.&lt;/p&gt;

&lt;p&gt;The code it produced had frequent errors. FiveM-specific APIs, QBCore patterns, the quirks of how server-side and client-side Lua communicate — it got a lot of it wrong. Not always. But enough that every session felt like a battle.&lt;/p&gt;

&lt;p&gt;The workflow looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Describe what I want&lt;/li&gt;
&lt;li&gt;Get code pasted into the chat window&lt;/li&gt;
&lt;li&gt;Manually copy it&lt;/li&gt;
&lt;li&gt;Create the file myself&lt;/li&gt;
&lt;li&gt;Test it, hit an error&lt;/li&gt;
&lt;li&gt;Paste the error back into chat&lt;/li&gt;
&lt;li&gt;Get a fix&lt;/li&gt;
&lt;li&gt;Hit a new error&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sometimes step 6-8 would loop five or six times before something actually worked. And every time I started a new chat, the context was gone. I'd have to re-explain what the script was supposed to do, what framework I was using, what had already been tried.&lt;/p&gt;

&lt;p&gt;I stuck with it for about a year. Not because it was great — because there wasn't a better option I trusted. I don't use Google products as a rule, so Gemini was never a consideration for me. And for a while, GPT was simply the only serious game in town.&lt;/p&gt;

&lt;p&gt;During that year I still managed to build things. The early versions of the billing system, some basic job scripts. But the process was exhausting. The ratio of "time thinking about the problem" to "time wrestling with AI output" was not in my favor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2 — Claude desktop app (~10-12 months ago)
&lt;/h2&gt;

&lt;p&gt;When I switched to Claude, the difference showed up almost immediately.&lt;/p&gt;

&lt;p&gt;The error rate dropped. Claude seemed to actually understand what I was building — not just the current request, but the context around it. When I said "this needs to work with QBCore's job system," it didn't just nod along and produce generic code. It produced code that actually accounted for how QBCore structures job data.&lt;/p&gt;

&lt;p&gt;But the change I noticed most wasn't about code quality. It was about output format.&lt;/p&gt;

&lt;p&gt;Claude could produce properly structured files — not just code blocks pasted into a chat window, but actual file-ready output I could use directly. That might sound like a small thing. It wasn't. The copy-paste-create cycle that had been eating time every single session started to shrink. The feedback loop got tighter. I could describe something, get a working file, test it, and move on.&lt;/p&gt;

&lt;p&gt;This was the point where something shifted in how I thought about AI. Before, it felt like using a very powerful search engine that could also write code. After Claude, it started feeling more like working with a collaborator — something that could hold context, reason about problems, and produce results I could actually use.&lt;/p&gt;

&lt;p&gt;I started giving Claude names. Opus became "おぷちゃん." Sonnet became "そねちゃん." Not just as a quirk — but because the relationship felt different. These weren't tools I was operating. They were teammates I was working with.&lt;/p&gt;

&lt;p&gt;The motto I built my whole approach around: &lt;strong&gt;AI to tomo ni&lt;/strong&gt; — working alongside AI, not just using it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3 — Claude Code (more recently)
&lt;/h2&gt;

&lt;p&gt;Then Claude Code arrived and changed everything again.&lt;/p&gt;

&lt;p&gt;The jump from GPT to Claude desktop was significant. The jump from Claude desktop to Claude Code was a different category of change entirely.&lt;/p&gt;

&lt;p&gt;With the desktop app, I was still doing the file work myself. Claude would produce the code, I would take it and put it where it needed to go. That was already a massive improvement over the GPT era. But there was still a gap between "AI produces output" and "work actually gets done."&lt;/p&gt;

&lt;p&gt;Claude Code closes that gap.&lt;/p&gt;

&lt;p&gt;I give it a path. It goes there. It reads the existing files, understands what's already been built, figures out where the new code fits. It makes the modifications. It creates new files. It builds folder structures. It handles the whole thing — not just the code, but the actual act of putting the code in the right place.&lt;/p&gt;

&lt;p&gt;The first time I used it, I remember thinking: I've never seen anything like this.&lt;/p&gt;

&lt;p&gt;Before Claude Code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Describe what I want&lt;/li&gt;
&lt;li&gt;Get code in a chat window&lt;/li&gt;
&lt;li&gt;Copy it manually&lt;/li&gt;
&lt;li&gt;Create the file myself&lt;/li&gt;
&lt;li&gt;Handle folder structure myself&lt;/li&gt;
&lt;li&gt;Come back to chat for the next piece&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After Claude Code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Describe what I want&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not an exaggeration. The scripts in this entire series — the billing system, the police reward distribution tool, the in-game arcade, the admin management tools, the vending machine system — were all built with this workflow. I design the system, work through the edge cases in my head, describe it clearly, and Claude Code handles implementation.&lt;/p&gt;

&lt;p&gt;It's not about saving keystrokes. It's about where your attention goes. When the implementation work is handled, I can focus entirely on the part that actually requires human judgment: what should this system do, how should players experience it, what happens when things go wrong, what did I miss.&lt;/p&gt;

&lt;p&gt;That's the work I want to be doing. That's the work I'm actually good at.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed — and what didn't
&lt;/h2&gt;

&lt;p&gt;Two years of AI-assisted development taught me a few things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tools got dramatically better.&lt;/strong&gt; The gap between GPT two years ago and Claude Code today is enormous. Anyone who wrote off AI coding tools based on early experiences should take another look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bottleneck shifted.&lt;/strong&gt; Early on, the bottleneck was AI quality — you'd spend most of your time fighting errors and unclear output. Now the bottleneck is specification quality. The clearer and more precise your description of what you want, the better the result. Garbage in, garbage out still applies — it's just that the bar for "good input" is now about clarity of thinking, not technical expertise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The collaboration model matters.&lt;/strong&gt; I've seen people treat AI like a vending machine — type a request, get output, complain when it's wrong. That's not how I work. I treat it like a teammate. I push back when something looks wrong. I explain the reasoning behind what I want. I ask for alternatives. The quality of what comes back is directly related to the quality of how you engage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't need to write code to build real things.&lt;/strong&gt; This might be the most important one. If you're a designer, a product thinker, or someone who understands systems but has never learned to code — the tools exist now to let you build real, working software. The gap between "I have an idea" and "this is running on my server" has never been smaller.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This series documented two years of building custom FiveM scripts. Five volumes, five scripts, one significant evolution in how I work.&lt;/p&gt;

&lt;p&gt;But FiveM was never the end goal. It was the practice ground.&lt;/p&gt;

&lt;p&gt;The same workflow — design-first, AI-implemented, human-reviewed — is what I'm bringing to web applications, PWAs, and freelance projects through my business Tack and K. The tools are the same. The philosophy is the same. The scale is just different.&lt;/p&gt;

&lt;p&gt;If you've been following this series and want to see what comes next, follow me here on Dev.to.&lt;/p&gt;

&lt;p&gt;And if you're a developer — or a non-developer who wants to build things — and you have questions about this workflow, drop a comment. I'm happy to talk through it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;"AI to tomo ni" — working alongside AI, not just using it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Full-Featured Vending Machine System for QBCore — Stock, Sales, and Discord Alerts</title>
      <dc:creator>Tack k</dc:creator>
      <pubDate>Thu, 19 Mar 2026 16:22:20 +0000</pubDate>
      <link>https://dev.to/tackk3/building-a-full-featured-vending-machine-system-for-qbcore-stock-sales-and-discord-alerts-ffk</link>
      <guid>https://dev.to/tackk3/building-a-full-featured-vending-machine-system-for-qbcore-stock-sales-and-discord-alerts-ffk</guid>
      <description>&lt;h2&gt;
  
  
  Why build a custom vending machine?
&lt;/h2&gt;

&lt;p&gt;Our server needed vending machines that weren't just static item dispensers. We wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Job-specific product lists (each business manages their own machine)&lt;/li&gt;
&lt;li&gt;Stock management by the job's employees&lt;/li&gt;
&lt;li&gt;Sales revenue tracking with cash-out by a boss&lt;/li&gt;
&lt;li&gt;Discord webhook notifications when items sell out&lt;/li&gt;
&lt;li&gt;Admin panel for placing and configuring machines in-game&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: &lt;strong&gt;lokat_vendex&lt;/strong&gt; — a full vending machine system built on QBCore with ox_inventory support.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The system has three layers of users:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customers&lt;/strong&gt; approach a machine, browse available products by job/shop, and purchase items with cash or bank funds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managers&lt;/strong&gt; (job members) deposit stock from their inventory into the machine, set prices, and withdraw accumulated sales revenue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admins&lt;/strong&gt; place new machines in-game, assign which jobs operate each machine, and adjust configurations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Job-based product separation
&lt;/h3&gt;

&lt;p&gt;Each machine can serve multiple jobs. Products are stored per &lt;code&gt;machine_id + job + item&lt;/code&gt;, so a machine shared between two businesses shows each business only their own products.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Products are scoped by machine and job&lt;/span&gt;
&lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vm_products&lt;/span&gt; &lt;span class="n"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;machine_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Race-safe stock deduction
&lt;/h3&gt;

&lt;p&gt;When a player purchases an item, stock is decremented using a conditional UPDATE that only succeeds if sufficient stock exists. This prevents two simultaneous purchases from taking more stock than available.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vm_products&lt;/span&gt;
&lt;span class="n"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt;
&lt;span class="n"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;machine_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the update affects 0 rows, the purchase is rejected rather than allowing negative stock.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partial refund on cart failure
&lt;/h3&gt;

&lt;p&gt;When buying multiple items in one cart, each item is processed individually. If one item fails (stock race, inventory full), only that item's cost is refunded — the rest of the cart succeeds. The player always ends up with exactly what they paid for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Event item purchase limits
&lt;/h3&gt;

&lt;p&gt;Items marked with the &lt;code&gt;event&lt;/code&gt; category have a per-player purchase cap, configurable in the server config. The cap resets on server restart. This is useful for limited-time event items.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventItemLimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;  &lt;span class="c1"&gt;-- max purchases per player per restart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Discord webhook on sell-out
&lt;/h3&gt;

&lt;p&gt;When any item's stock hits zero (from a purchase or manual withdrawal), the script sends a webhook notification to the relevant Discord channel. Each job can have its own webhook URL configured separately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Example config structure (use your own webhook URLs)&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebhookByJob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;your_job_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook payload includes the job name, item name, and who triggered the sell-out — so the right team gets notified automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  In-game machine placement
&lt;/h3&gt;

&lt;p&gt;Admins can place machines without touching config files. The placement flow works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the admin panel with a command&lt;/li&gt;
&lt;li&gt;Select a position using an in-game preview prop&lt;/li&gt;
&lt;li&gt;Assign jobs that will operate the machine&lt;/li&gt;
&lt;li&gt;Confirm — the machine is saved to the database and instantly synced to all players&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The preview prop follows the admin's character in real-time, showing the interaction radius as a marker. Rotation and radius are adjustable before confirming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dual target support
&lt;/h3&gt;

&lt;p&gt;Works with both &lt;code&gt;qb-target&lt;/code&gt; and &lt;code&gt;ox_target&lt;/code&gt;. Configured with a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'qb-target'&lt;/span&gt;  &lt;span class="c1"&gt;-- or 'ox_target'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cash or bank payment
&lt;/h3&gt;

&lt;p&gt;Players can pay with cash, bank, or auto mode (cash first, bank fallback). Configurable per server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'auto'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- 'cash' | 'bank' | 'auto'&lt;/span&gt;
    &lt;span class="n"&gt;payoutMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'bank'&lt;/span&gt;   &lt;span class="c1"&gt;-- how managers receive their sales payout&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Database schema overview
&lt;/h2&gt;

&lt;p&gt;Four tables handle the full lifecycle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vm_machines&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Machine locations, labels, assigned jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vm_products&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-machine, per-job inventory and pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vm_balances&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accumulated sales revenue per machine per job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vm_ledger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full audit log of every transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ledger table records every sale, deposit, withdrawal, and adjustment with actor name and timestamp — useful for dispute resolution and monitoring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security design
&lt;/h2&gt;

&lt;p&gt;All permission checks run server-side. The client never decides whether an action is allowed — it only sends requests. The server validates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin operations require ACE permission &lt;code&gt;lokat_vendex.admin&lt;/code&gt; or QBCore admin/god flags&lt;/li&gt;
&lt;li&gt;Manager operations (stock, payout) check that the player's current job matches the machine's assigned jobs&lt;/li&gt;
&lt;li&gt;Boss-only operations (collect all sales) additionally check &lt;code&gt;job.isboss&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A debounce cooldown blocks rapid duplicate submissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Client-side &lt;code&gt;canInteract&lt;/code&gt; checks are for UI only — they hide options from unauthorized players but are never trusted by the server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Vol.6&lt;/strong&gt; — wrapping up the series with a look at how all these scripts fit together as a server-management ecosystem, and what I'd do differently if building from scratch today.&lt;/p&gt;




&lt;h2&gt;
  
  
  The AI behind this
&lt;/h2&gt;

&lt;p&gt;This script — like everything in this series — was built with AI as my coding partner.&lt;br&gt;
I don't write Lua myself. I design the systems, think through the edge cases,&lt;br&gt;
and the AI handles implementation.&lt;/p&gt;

&lt;p&gt;But getting here wasn't straightforward. I went through ChatGPT, Gemini,&lt;br&gt;
and a few others before landing on Claude Code as my go-to.&lt;br&gt;
Each had its strengths, but Claude Code was the one that actually stuck.&lt;/p&gt;

&lt;p&gt;That story deserves its own post — coming soon.&lt;/p&gt;




&lt;p&gt;Questions about the vending machine system? Drop a comment.&lt;/p&gt;

&lt;p&gt;Questions about the vending machine system? Drop a comment.&lt;/p&gt;

</description>
      <category>fivem</category>
      <category>qbcore</category>
      <category>lua</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Two Admin Scripts That Saved Me Hours of Server Management in QBCore</title>
      <dc:creator>Tack k</dc:creator>
      <pubDate>Wed, 18 Mar 2026 17:25:46 +0000</pubDate>
      <link>https://dev.to/tackk3/two-admin-scripts-that-saved-me-hours-of-server-management-in-qbcore-27eb</link>
      <guid>https://dev.to/tackk3/two-admin-scripts-that-saved-me-hours-of-server-management-in-qbcore-27eb</guid>
      <description>&lt;h2&gt;
  
  
  The problem with managing a live RP server
&lt;/h2&gt;

&lt;p&gt;Running a QBCore RP server means dealing with two recurring headaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Players get stuck in the wrong job, or you need to assign roles fast during a session — and doing it through the database or existing admin menus is slow.&lt;/li&gt;
&lt;li&gt;Inactive players accumulate in the database over months. Ghosts with vehicles, characters, and data that will never be used again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I built two scripts to solve both: &lt;strong&gt;lokat_adminjob&lt;/strong&gt; for real-time job management, and &lt;strong&gt;lokat_cleanup&lt;/strong&gt; for database pruning.&lt;/p&gt;




&lt;h2&gt;
  
  
  lokat_adminjob — Job assignment from a UI panel
&lt;/h2&gt;

&lt;p&gt;The idea is simple: open a panel with a command, pick a player from the online list, pick a job and grade, and apply it instantly. No database queries, no server restarts, no fumbling through existing admin menus.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permission system
&lt;/h3&gt;

&lt;p&gt;The script checks permissions in two layers — txAdmin ACE first, then QBCore's native admin/god flags. This means it works whether you're running a txAdmin-managed server or a vanilla QBCore setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;IsPlayerAceAllowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AceName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;QBCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;QBCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'god'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permission checks always run server-side. The client never makes the decision — it just sends requests. If someone tries to call the server event directly without permission, they get blocked.&lt;/p&gt;

&lt;h3&gt;
  
  
  ps-multijob integration
&lt;/h3&gt;

&lt;p&gt;This is the part that saves the most headaches. If a player has the same job as a side job in ps-multijob and you assign it as their main job, you get a conflict. The script handles this automatically — before setting a new main job, it removes any matching side job entry from ps-multijob.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Remove conflicting side job before assigning main job&lt;/span&gt;
&lt;span class="n"&gt;PMJ_RemoveJobByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When removing a job entirely (setting to unemployed), it clears all side jobs too — one clean operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the panel shows
&lt;/h3&gt;

&lt;p&gt;When an admin opens the panel, the server sends a live snapshot: all available jobs with their grade levels, and all currently online players with their current job. The admin selects a player, selects a job and grade, and submits — the change applies immediately with notifications sent to both the admin and the target player.&lt;/p&gt;




&lt;h2&gt;
  
  
  lokat_cleanup — Inactive player pruning tool
&lt;/h2&gt;

&lt;p&gt;Over time, inactive players pile up in the database. After years of server operation, you end up with hundreds of accounts that haven't logged in for months — each with characters, vehicles, and associated data.&lt;/p&gt;

&lt;p&gt;lokat_cleanup is an admin UI that lets you find and delete these accounts safely.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Open the UI with a command, set the inactivity threshold (default: 60 days), and the script queries the database for accounts where all characters' &lt;code&gt;last_updated&lt;/code&gt; timestamp is older than the threshold.&lt;/p&gt;

&lt;p&gt;Results are grouped by license (one row per account, not per character) and show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Character names linked to the account&lt;/li&gt;
&lt;li&gt;Last active date&lt;/li&gt;
&lt;li&gt;Number of vehicles owned&lt;/li&gt;
&lt;li&gt;Total character count
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Group by license, get the most recent activity across all characters&lt;/span&gt;
&lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt; &lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;
&lt;span class="n"&gt;GROUP&lt;/span&gt; &lt;span class="n"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt;
&lt;span class="n"&gt;HAVING&lt;/span&gt; &lt;span class="n"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Safety checks
&lt;/h3&gt;

&lt;p&gt;Before deleting any account, the server checks whether any character on that license is currently online. If they are, the delete is blocked — no accidents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QBCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPlayerByCitizenId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;citizenid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'player_online'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also choose whether to delete associated vehicles — toggled per deletion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search and pagination
&lt;/h3&gt;

&lt;p&gt;With large player databases, listing everything at once isn't practical. The UI supports character name search and pagination. Results can be sorted oldest-first or newest-first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Config overview
&lt;/h2&gt;

&lt;p&gt;Both scripts use a simple config file. Key settings:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;lokat_adminjob:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your_ace_permission_name'&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your_command_name'&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MultiJob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;lokat_cleanup:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinDaysInactiveDefault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;  &lt;span class="c1"&gt;-- adjust to your needs&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PageSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseAcePermission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your_ace_permission_name'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why these exist
&lt;/h2&gt;

&lt;p&gt;Most admin tasks in QBCore require either database access or navigating menus that weren't designed for speed. These two scripts are purpose-built for the specific operations I was doing repeatedly — and they eliminated a lot of friction from day-to-day server management.&lt;/p&gt;

&lt;p&gt;Next up: &lt;strong&gt;Vol.5 — Building a Custom Vending Machine System for QBCore.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Built with Claude Opus as my coding partner. I handle system design and edge case thinking — the AI handles implementation.&lt;/p&gt;

&lt;p&gt;Questions about the implementation? Drop a comment.&lt;/p&gt;

</description>
      <category>fivem</category>
      <category>qbcore</category>
      <category>lua</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I Built a Playable Arcade Inside My FiveM Server — Tetris, Breakout, Space Invaders and More</title>
      <dc:creator>Tack k</dc:creator>
      <pubDate>Wed, 18 Mar 2026 04:22:40 +0000</pubDate>
      <link>https://dev.to/tackk3/i-built-a-playable-arcade-inside-my-fivem-server-tetris-breakout-space-invaders-and-more-25dd</link>
      <guid>https://dev.to/tackk3/i-built-a-playable-arcade-inside-my-fivem-server-tetris-breakout-space-invaders-and-more-25dd</guid>
      <description>&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Our QBCore RP server had an arcade cabinet prop sitting in a few locations around the map. It was purely decorative. Players would walk past it every session and nothing happened.&lt;/p&gt;

&lt;p&gt;I wanted it to actually work — walk up, press E, and play a real game inside the game.&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;exs-arcade&lt;/strong&gt; — a fully playable in-game arcade system built with Svelte + TypeScript for the UI, Lua for FiveM integration, and a server-side ranking system backed by MySQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's inside
&lt;/h2&gt;

&lt;p&gt;Four playable games, each with its own engine and Svelte component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Block Puzzle&lt;/strong&gt; (Tetris) — classic falling block game&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alien Shooter&lt;/strong&gt; (Space Invaders) — wave-based shooter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brick Breaker&lt;/strong&gt; (Breakout) — paddle and ball&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Racing&lt;/strong&gt; — time attack mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus a global ranking board that tracks top scores per game and an overall leaderboard combining all scores.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The architecture is split into three layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lua client&lt;/strong&gt; detects nearby arcade cabinet props and shows an interaction prompt. When the player presses E, it freezes their character and opens the NUI (web-based UI overlay).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetClosestObjectOfType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InteractDistance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CabinetModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;DrawText3D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objCoords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objCoords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objCoords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[E] Play Arcade'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;IsControlJustReleased&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="c1"&gt;-- E key&lt;/span&gt;
        &lt;span class="n"&gt;FreezeEntityPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerPedId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;openArcade&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Svelte NUI&lt;/strong&gt; handles the full game UI — main menu, individual game screens, score submission, and the ranking board. Each game is a self-contained Svelte component with its own TypeScript engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js server&lt;/strong&gt; handles score submission, validation, and ranking queries via oxmysql. It also deduplicates rankings so only each player's personal best counts toward the leaderboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ranking system
&lt;/h2&gt;

&lt;p&gt;Scores are stored in MySQL with a &lt;code&gt;game_id&lt;/code&gt; field. The server deduplicates by player so one person can't flood the top 10 — only their best run counts.&lt;/p&gt;

&lt;p&gt;For racing, scores are sorted ascending (lower time = better). For all other games, scores are sorted descending. The server handles this automatically based on game type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;TIME_ATTACK_GAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;racing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;SCORE_GAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tetris&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invaders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;breakout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isTimeAttack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gameId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DESC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also an overall leaderboard that sums each player's personal bests across all score-based games — giving a "best arcade player on the server" title.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cabinet detection — two modes
&lt;/h2&gt;

&lt;p&gt;The script supports two ways to trigger the arcade:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prop-based (default):&lt;/strong&gt; Automatically detects any &lt;code&gt;prop_arcade_01&lt;/code&gt; object within range. No coordinate setup needed — if the prop is in the world, it works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Location-based:&lt;/strong&gt; Define exact coordinates in config for tighter control over where the arcade is accessible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseLocations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;-- true = coordinate mode, false = prop detection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Tech stack breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FiveM client&lt;/td&gt;
&lt;td&gt;Lua (QBCore)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI framework&lt;/td&gt;
&lt;td&gt;Svelte 5 + TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build tool&lt;/td&gt;
&lt;td&gt;Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server logic&lt;/td&gt;
&lt;td&gt;Node.js (FiveM native)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;MySQL via oxmysql&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Building the UI in Svelte rather than plain HTML/JS made the game state management dramatically cleaner. Each game component owns its own engine instance and lifecycle — no global state leaks between games.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;A rhythm game component is already stubbed out in the config (marked &lt;code&gt;enabled: false&lt;/code&gt;). That's Phase 4. For now the four working games keep players busy.&lt;/p&gt;

&lt;p&gt;Next up in this series: &lt;strong&gt;Vol.4 — Customizing ox_inventory for QBCore: adding item categories, weight display, and a custom search UI.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Built with Claude Opus as my coding partner. I designed the system architecture and UX — the AI handled implementation. That's how this whole series works.&lt;/p&gt;

&lt;p&gt;Questions or want to see a specific game engine in detail? Drop a comment.&lt;/p&gt;

</description>
      <category>fivem</category>
      <category>qbcore</category>
      <category>lua</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Building a Police Reward Distribution System with PED Interaction in QBCore</title>
      <dc:creator>Tack k</dc:creator>
      <pubDate>Tue, 17 Mar 2026 01:18:57 +0000</pubDate>
      <link>https://dev.to/tackk3/building-a-police-reward-distribution-system-with-ped-interaction-in-qbcore-l5i</link>
      <guid>https://dev.to/tackk3/building-a-police-reward-distribution-system-with-ped-interaction-in-qbcore-l5i</guid>
      <description>&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;In our QBCore RP server, police officers would arrest suspects and collect fines. But splitting that money fairly among everyone who responded to the call was always a manual headache — someone had to calculate the split and do individual transfers.&lt;/p&gt;

&lt;p&gt;I wanted a system where an officer could walk up to a specific NPC, enter the total amount, select which colleagues participated, and have the money split and distributed automatically.&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;lokat_sharing_money&lt;/strong&gt; — a PED-based fund distribution script with job authorization, grade checks, on-duty filtering, and dual banking support.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Interact with the PED.&lt;/strong&gt; A security guard NPC is placed at the police station. Officers approach and interact via qb-target. The &lt;code&gt;canInteract&lt;/code&gt; check runs client-side to hide the option from unauthorized players.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;canInteract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;PD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QBCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPlayerData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;grade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;

    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;okJob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthorizedJobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;okJob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;okJob&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinGradeToStart&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;grade&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequireOnDutyToStart&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;PD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onduty&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 — Enter the amount.&lt;/strong&gt; A qb-input dialog asks for the total amount to distribute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Select recipients.&lt;/strong&gt; The server returns a list of online players with the same job who are currently on-duty. The officer selects who participated using a checkbox-style qb-menu.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Server: filter same job, on-duty players&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QBCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;dutyOK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnDutyOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PlayerData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onduty&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dutyOK&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PlayerData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;myJob&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="nb"&gt;table.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;" "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4 — Confirm and distribute.&lt;/strong&gt; A summary screen shows the total and per-person amount. On confirm, the server deducts from the initiator's bank and distributes equally to each selected officer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;perAmount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;math.floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="n"&gt;selectedPlayers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selectedPlayers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;receiver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QBCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;receiver&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;receiver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddMoney&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;perAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"job-distribution-received"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key design decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Job is inferred server-side.&lt;/strong&gt; The client never sends which job to filter by — the server reads the initiator's job directly. This prevents players from spoofing a different job to access the distribution pool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-duty check at distribution time.&lt;/strong&gt; Even if someone was selected as a recipient, the server re-checks their on-duty status at payment time. If they clocked out between selection and confirmation, they get skipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dual banking support.&lt;/strong&gt; Works with both qb-banking and okokBanking. When okokBanking is selected, a transaction log entry is also added for each recipient.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BankingSystem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"okokBanking"&lt;/span&gt;  &lt;span class="c1"&gt;-- or "qb-banking"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Multi-language support.&lt;/strong&gt; All UI strings are defined in a locale table. Switch between Japanese and English with a single config value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"en"&lt;/span&gt;  &lt;span class="c1"&gt;-- or "ja"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Config overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthorizedJobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'police'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ambulance'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mechanic'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinGradeToStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;police&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- sergeant and above&lt;/span&gt;
    &lt;span class="n"&gt;ambulance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mechanic&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- any grade&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnDutyOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequireOnDutyToStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Peds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s_m_m_security_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;coords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;438&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;991&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;69&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;215&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Built with AI, designed by a non-coder
&lt;/h2&gt;

&lt;p&gt;Like everything in this series, I designed the UX flow and logic, and implemented it with Claude Opus as my coding partner. The tricky parts — server-side job inference, double on-duty checks, banking abstraction — all came from thinking through edge cases before writing a single line.&lt;/p&gt;

&lt;p&gt;Next up: &lt;strong&gt;Vol.3 — Building an In-Server Arcade with Tetris, Breakout, and Space Invaders in FiveM.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Questions or want the full source? Drop a comment below.&lt;/p&gt;

</description>
      <category>fivem</category>
      <category>qbcore</category>
      <category>lua</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I Built a Custom Billing System for My QBCore Server — Here's What I Learned</title>
      <dc:creator>Tack k</dc:creator>
      <pubDate>Mon, 16 Mar 2026 04:06:49 +0000</pubDate>
      <link>https://dev.to/tackk3/i-built-a-custom-billing-system-for-my-qbcore-server-heres-what-i-learned-deg</link>
      <guid>https://dev.to/tackk3/i-built-a-custom-billing-system-for-my-qbcore-server-heres-what-i-learned-deg</guid>
      <description>&lt;h2&gt;
  
  
  Why I built my own
&lt;/h2&gt;

&lt;p&gt;My QBCore server had a very specific need: mechanics needed to bill players for multiple repair items in a single invoice, with each item having a variable quantity. Something like "Engine repair × 3, Tire replacement × 4" — calculated on the fly, not pre-defined in config.&lt;/p&gt;

&lt;p&gt;I also wanted job-based revenue splitting baked in — so when a bill gets paid, the money automatically goes to both the employee and the company society account at the right ratio.&lt;/p&gt;

&lt;p&gt;I couldn't find an existing solution that did exactly what I wanted, so I built &lt;strong&gt;gg_billing2&lt;/strong&gt; from scratch using Claude Opus as my coding partner.&lt;/p&gt;




&lt;h2&gt;
  
  
  What gg_billing2 does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Quantity-aware billing.&lt;/strong&gt; Players select items from a job-specific list and input quantities. The total is calculated live. No need to pre-define every price combination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job-based revenue split.&lt;/strong&gt; Each job has a configurable company/personal share ratio. Payment is automatically distributed at the moment the bill is settled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JobShares&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;police&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- 50% company, 50% personal&lt;/span&gt;
    &lt;span class="n"&gt;mechanic&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- 70% company, 30% personal&lt;/span&gt;
    &lt;span class="n"&gt;ambulance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Due dates and overdue handling.&lt;/strong&gt; Bills expire after a configurable number of days per job. You can choose to allow late payment, block it, or charge a late fee percentage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OverdueBehavior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'late_fee'&lt;/span&gt;

&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LateFee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;type&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'percent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 10% late fee&lt;/span&gt;
    &lt;span class="n"&gt;toCompanyOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Repeat overdue notifications.&lt;/strong&gt; Debtors get notified on a configurable interval while their bill remains unpaid and overdue. Also triggers on login.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mass invoicing.&lt;/strong&gt; Send the same bill to multiple players at once — handy for group fines after large RP events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dual banking support.&lt;/strong&gt; Works with both okokBanking and qb-banking via a single config flag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;useOkokBanking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;-- false = qb-banking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The trickiest part: split calculation
&lt;/h2&gt;

&lt;p&gt;Revenue splitting sounds simple, but there's a subtle edge case: what if the config changes between when a bill is created and when it's paid? To handle this, gg_billing2 saves the job share at invoice creation time and uses that saved value when calculating the split — regardless of what the current config says.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Uses the share saved at invoice creation time&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;jobShare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;companyShareUsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;GetJobShare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;companyAmount&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;math.floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payBase&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;jobShare&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;personalAmount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payBase&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;companyAmount&lt;/span&gt;

&lt;span class="n"&gt;xOwner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddMoney&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bank'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;personalAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'billing-receive-personal'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AddCompanyMoney&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;companyAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'billing-receive-company'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Built with AI, designed by a non-coder
&lt;/h2&gt;

&lt;p&gt;I designed the entire system — the data model, UX flow, and edge cases — and implemented it using Claude Opus as my coding partner. I don't write Lua myself; my job is systems thinking and design.&lt;/p&gt;

&lt;p&gt;This is Vol.1 of my FiveM Dev Diaries series, documenting 2+ years of custom scripts built for my QBCore server.&lt;/p&gt;

&lt;p&gt;Next up: &lt;strong&gt;Vol.2 — A Police Reward Distribution System with PED-based interaction UI.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Questions about the implementation? Drop a comment — happy to share config snippets or dig into specifics.&lt;/p&gt;

</description>
      <category>fivem</category>
      <category>qbcore</category>
      <category>lua</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I Don't Write Code. I Lead an AI Team That Does.</title>
      <dc:creator>Tack k</dc:creator>
      <pubDate>Sun, 15 Mar 2026 15:53:46 +0000</pubDate>
      <link>https://dev.to/tackk3/i-dont-write-code-i-lead-an-ai-team-that-does-leo</link>
      <guid>https://dev.to/tackk3/i-dont-write-code-i-lead-an-ai-team-that-does-leo</guid>
      <description>&lt;h2&gt;
  
  
  Let me be honest with you
&lt;/h2&gt;

&lt;p&gt;I'm an engineer with over 11 years of experience. I've shipped reservation systems, PWAs, FiveM game servers, and electronic health record apps. I design architecture, define data flows, and make product decisions every day.&lt;/p&gt;

&lt;p&gt;But I don't write the code myself. My AI team does.&lt;/p&gt;

&lt;p&gt;This isn't a confession. It's a workflow — and I think it's the future of solo engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Meet the team
&lt;/h2&gt;

&lt;p&gt;My setup is simple. I use Claude Opus for everything, in two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧠 &lt;strong&gt;Me&lt;/strong&gt; — product design, system architecture, UX decisions&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Claude Opus (chat)&lt;/strong&gt; — turns my rough ideas into detailed spec docs and implementation instructions&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Claude Code (Opus)&lt;/strong&gt; — reads the spec and actually builds it, file by file&lt;/li&gt;
&lt;li&gt;🔁 &lt;strong&gt;Me again&lt;/strong&gt; — review, test, iterate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One model. Two roles. Zero context switching between different AI tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "teammate" — not "tool"
&lt;/h2&gt;

&lt;p&gt;Most developers talk about AI like it's a fancy autocomplete. I don't see it that way.&lt;/p&gt;

&lt;p&gt;I gave my AI a name. I treat its outputs like a colleague's pull request — I review them, push back when something's wrong, and give feedback. When the spec isn't right, I don't just re-prompt. I argue with it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My motto is: &lt;strong&gt;"AI to tomo ni"&lt;/strong&gt; — working &lt;em&gt;alongside&lt;/em&gt; AI, not just using it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That mindset shift changes everything. You stop asking "did it do what I said?" and start asking "is this actually the right solution?" That's where quality comes from.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed when I switched to this model
&lt;/h2&gt;

&lt;p&gt;I'm a day-job engineer running a freelance side business. Without AI, I'd be stuck — I can design systems in my head, but I'd never ship fast enough to compete.&lt;/p&gt;

&lt;p&gt;Now I can take on projects solo that would normally require a team. Not because AI replaced me — but because it handles the part I was never good at, so I can focus on the part I am.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest trade-offs
&lt;/h2&gt;

&lt;p&gt;This workflow isn't perfect. Claude Code still needs clear, structured specs — vague instructions produce vague code. My job is to think precisely so the AI can execute precisely. Garbage in, garbage out still applies.&lt;/p&gt;

&lt;p&gt;But the leverage is real. And if you're a designer, PM, or systems thinker who's been told "you need to learn to code" — maybe you don't. Maybe you just need the right team.&lt;/p&gt;




&lt;p&gt;I'm Taku, a freelance full-stack engineer based in Japan. I build web apps, PWAs, and game server scripts — mostly with an AI co-pilot. If this resonates, follow along. More posts coming on FiveM dev and PHP system architecture.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>freelancing</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
