14. Dokumen Teknis Programmer

← Kembali ke indeks

Dokumen ini menerjemahkan desain menjadi arsitektur untuk V1. Angka gameplay tidak ditulis di kode. Semuanya ada di modul config (bagian 4) dan bersumber dari 08_Ekonomi.md.


1. Keputusan teknis kunci

#KeputusanAlasan
1Case disimpan sebagai recipe (data), bukan gambar. Case = {device, shape, baseColor, finish, pattern, patternColor, stickers[]}Kecil untuk DataStore, bisa dirender ulang di mana saja (bench, rak, tangan avatar, galeri, Hall of Fame), aman dari moderasi UGC karena semua bahan sudah disetujui
2Tanpa EditableImage di V1. Painting memakai grid sel + SurfaceGui sementara, hasil akhir = warna solid + decalEditableImage masih beta, dibatasi memori, dan mewajibkan verifikasi umur/ID [SRC Analisis S23]. Free-paint ditunda
3Server-authoritative untuk ekonomi. Client hanya mengirim pilihan tiap langkah. Server memvalidasi, mengukur waktu kerja sendiri, menghitung bonus, cash, dan XPMencegah exploit cash/XP
4Semua angka di ModuleScript config (ReplicatedStorage.Shared.Config)Prinsip game sumber: semua angka di tabel [SRC 08]. Siap dipindah ke remote config di V1.1
5NPC divisualkan di client, logika di server8 plot Γ— Β±10 NPC = Β±80 NPC. Server hanya menyimpan state ringan, client merender model + animasi untuk plot yang dekat
6ProfileStore untuk data pemainSession locking, auto-save, aman dari data hilang/duplikasi
7StreamingEnabled aktifDevice mobile low-end

2. Stack & tooling

BagianPilihan
BahasaLuau --!strict
Sinkronisasi fileRojo + Git
PackageWally: ProfileStore, Signal (GoodSignal), Promise, t (validasi tipe remote)
UIReact-Lua (jsdotlua/react) atau Fusion. Pilih satu di awal dan jangan dicampur
TestJest-Lua / TestEZ untuk modul murni (rumus ekonomi, dispatch, order generator)
LintSelene + StyLua

3. Struktur folder (Rojo)

src/
β”œβ”€ ReplicatedStorage/Shared/
β”‚  β”œβ”€ Config/            -- semua angka desain (bagian 4)
β”‚  β”œβ”€ Types.luau         -- tipe Order, CaseRecipe, PlayerData, dll.
β”‚  β”œβ”€ Formulas.luau      -- rumus gaji/bonus/XP/AFK (murni, dites)
β”‚  β”œβ”€ CaseBuilder.luau   -- recipe β†’ Model case (dipakai client & server)
β”‚  └─ Remotes.luau       -- definisi semua RemoteEvent/Function
β”œβ”€ ServerScriptService/
β”‚  β”œβ”€ Services/
β”‚  β”‚  β”œβ”€ DataService        -- ProfileStore, load/save, migrasi skema
β”‚  β”‚  β”œβ”€ PlotService        -- assign/reset plot, build tycoon & dekor
β”‚  β”‚  β”œβ”€ ShopService        -- customer spawn, kasir, dispatch, waiting table
β”‚  β”‚  β”œβ”€ OrderService       -- order generator, validasi craft, reward
β”‚  β”‚  β”œβ”€ EmployeeService    -- timer kerja karyawan, upgrade
β”‚  β”‚  β”œβ”€ ProgressionService -- XP, level, unlock
β”‚  β”‚  β”œβ”€ AfkService         -- Tip Jar, offline income
β”‚  β”‚  β”œβ”€ SocialService      -- like, visit, bonus teman/grup/premium
β”‚  β”‚  β”œβ”€ GalleryService     -- Best Case of the Week, Hall of Fame
β”‚  β”‚  β”œβ”€ MonetizationService-- gamepass, ProcessReceipt, gems
β”‚  β”‚  β”œβ”€ QuestService       -- daily login, daily quest, codes
β”‚  β”‚  └─ AnalyticsService   -- wrapper AnalyticsService Roblox
β”‚  └─ Main.server.luau
└─ StarterPlayer/StarterPlayerScripts/
   β”œβ”€ Controllers/
   β”‚  β”œβ”€ CraftController    -- kamera tetap di Craft Bench, rotasi case per langkah, tray HUD, mini-game
   β”‚  β”œβ”€ Steps/             -- ShapeStep, PaintStep, SmoothStep, PatternStep, StickerStep, CleanStep
   β”‚  β”œβ”€ NpcController      -- render pelanggan, karyawan, kasir dari state server
   β”‚  β”œβ”€ GuideController    -- panah kuning, cincin, tutorial
   β”‚  β”œβ”€ InputController    -- PreferredInput, mapping kontrol
   β”‚  └─ AudioController    -- SoundGroup, variasi, ducking
   └─ UI/                   -- HUD, Phone + apps, CraftScreen, Reward, LevelUp

4. Config modules

ModulIsiSumber
LevelsxpToNext, unlock per level (customer, device, decor, tycoon, colors, patterns, stickers, shapes), gems level-up07
Devicesid, nama tampil, baseCash, timeFactor, daftar shapes06
Stepsid, cash, unlockLevel, minDurationSec, kata konfirmasi, skill karyawan04, 08
Customersid, cashMult, xpMult, bobot per level, jumlah elemen request, aturan khusus06
Colors / Patterns / Stickers / Shapesid, nama, hex/aset, unlockLevel, premiumGems?07, 15
Tycoonid, level, harga, efek05
Decorslot, varian (Basic/Cute/Premium), harga, level05
Employeesharga rekrut, skill & biaya, tabel Speed & Quality05
Economybonus kesesuaian (0,25) & kecepatan (0,20), pengali target L1–2 (1,5), bonus sosial, AFK rate & cap, harga Instant Finish/Rush, login, quest08
MonetizationGamePassId, ProductId, isi produk09

Contoh:

-- Config/Steps.luau
return {
  -- cash = gaji dasar langkah, target = detik untuk bonus kecepatan, minSec = batas anti-exploit
  shape   = { cash = 4,  target = 5,  unlock = 1, minSec = 1.0, word = "PERFECT FIT!", skill = "shape"   },
  paint   = { cash = 6,  target = 18, unlock = 1, minSec = 3.0, word = "COLORFUL!",    skill = "paint"   },
  smooth  = { cash = 4,  target = 16, unlock = 1, minSec = 3.0, word = "SMOOTH!",      skill = "smooth"  },
  glossy  = { cash = 10, target = 6,  unlock = 6, minSec = 1.5, word = "SHINY!",       skill = "glossy", sub = true },
  pattern = { cash = 8,  target = 16, unlock = 2, minSec = 2.0, word = "SPARKLY!",     skill = "pattern" },
  sticker = { cash = 8,  target = 22, unlock = 4, minSec = 1.0, word = "CUTE!",        skill = "sticker" },
  clean   = { cash = 3,  target = 12, unlock = 1, minSec = 2.0, word = "SPOTLESS!",    skill = "clean"   },
}

5. Model data

5a. CaseRecipe

export type CaseRecipe = {
  v: number,                 -- versi skema recipe
  device: string,            -- "aphone" | "samsam" | "earpod" | "tablet" | "macbook"
  shape: string,
  baseColor: string,         -- id warna (bukan RGB)
  finish: "matte" | "glossy",
  pattern: string?, patternColor: string?,
  stickers: { { id: string, u: number, v: number, s: number, r: number } }, -- u,v 0..1; s 0.6..1.4; r derajat
}

Ukuran Β±150–300 byte (JSON). Album 30 case β‰ˆ 9 KB.

5b. Order

export type Order = {
  id: string, plotId: number,
  customerType: string, device: string,
  steps: { string },                  -- urutan langkah wajib, mis. {"shape","paint","smooth","pattern","clean"}
  request: { shape: string?, baseColor: string?, pattern: string?, patternColor: string?,
             sticker: string?, finish: string? },
  pairIndex: number?,                 -- Old Club: 1 atau 2
  baseSalary: number,                 -- gaji dasar, dihitung server saat order dibuat
  targetSec: number,                  -- waktu target bonus kecepatan
  activeSec: number,                  -- waktu kerja terukur server (tidak termasuk PUT DOWN)
  state: OrderState, assignee: number?, -- userId pemain atau index karyawan
  createdAt: number, startedAt: number?,
}

5c. PlayerData (ProfileStore, skema v1)

{
  schema = 1,
  level = 1, xp = 0, prestige = 0,
  cash = 0, gems = 0,
  tycoon = { patternShelf = false, emp1 = false, stickerStock = false, glossyShelf = false,
             emp2 = false, waitingExpand = false, emp3 = false },
  decor = { stool = "basic", table = nil, ... , wallPaint = "cream", floor = "checker_pink", ... },
  ownedDecorVariants = { ["stool.cute"] = true, ... },
  employees = { { name = "Jules", skills = {pattern=true}, speed = 1, quality = 1 }, ... },
  album = { CaseRecipe },            -- maks 30, FIFO
  display = { albumIndex | nil x8 }, -- slot rak
  equipped = albumIndex?,
  tipJar = { amount = 0, lastTick = os.time() },
  premiumOwned = { holoGlitter = true, ... },
  daily = { loginDay = 0, lastLoginDay = 0, quests = {...}, questDay = 0 },
  gallery = { week = 202645, submitted = albumIndex?, pendingRewards = {...} },
  likesGivenToday = 0, likeDay = 0,
  badges = {}, codes = {},
  stats = { casesCrafted = 0, thievesCaught = 0, ... },
  receipts = { [purchaseId] = true },  -- untuk idempotensi (simpan 100 terakhir)
  flags = { ftueStep = 0 },
}

Setiap perubahan skema menaikkan schema dan menambah fungsi migrasi di DataService.


6. State machine

6a. Pelanggan

SPAWN β†’ WALK_TO_CASHIER β†’ AT_CASHIER ──dispatch──► (EMPLOYEE | WAITING_TABLE | BACKLOG_SEAT)
   BACKLOG_SEAT ──slot/karyawan tersedia──► (EMPLOYEE | WAITING_TABLE)
   (order selesai, sudah dibayar) β†’ WALK_TO_BENCH β†’ PICKUP (reaksi) β†’ [THIEF_RUN?] β†’ LEAVE β†’ DESPAWN

6b. Order

CREATED β†’ ASSIGNED_EMPLOYEE β†’ EMPLOYEE_WORKING β†’ PAID (langsung saat selesai) β†’ PICKUP (visual)
CREATED β†’ ON_WAITING_TABLE β†’ TAKEN_BY_PLAYER β†’ CRAFTING ⇄ PUT_DOWN_ON_BENCH
                                         β†’ PAID (langsung saat langkah terakhir selesai) β†’ PICKUP (visual)
CREATED β†’ BACKLOG β†’ (ASSIGNED_EMPLOYEE | ON_WAITING_TABLE)

6c. Karyawan

IDLE β†’ (ShopService.assign) β†’ WALK_TO_BENCH β†’ WORKING(durasi = Speed) β†’ PAID (roll bonus Quality & Speed) β†’ IDLE

6d. Dispatch (ShopService)

local function canDo(emp, order)
  for _, step in order.steps do
    if not (BASE_SKILLS[step] or emp.skills[step]) then return false end
  end
  return true
end

function ShopService.onCustomerAtCashier(plot, order)
  -- karyawan idle yang mampu, pilih yang skill-nya paling sedikit
  local best, bestCount = nil, math.huge
  for i, emp in plot.employees do
    if emp.state == "IDLE" and canDo(emp, order) then
      local n = countSkills(emp)
      if n < bestCount then best, bestCount = i, n end
    end
  end
  if best then return assignToEmployee(plot, best, order) end
  if #plot.waitingTable < Config.Shop.waitingTableCap(plot) then
    return putOnWaitingTable(plot, order)
  end
  return putInBacklog(plot, order)
end

function ShopService.onEmployeeIdle(plot, empIndex)
  local order = firstInBacklog(plot, function(o) return canDo(plot.employees[empIndex], o) end)
  if order then assignToEmployee(plot, empIndex, order) end
end

function ShopService.onWaitingTableSlotFreed(plot)
  local order = popBacklogForTable(plot) or spawnNewCustomer(plot) -- "auto create new antrian"
  if order then putOnWaitingTable(plot, order) end
end

Spawner: setiap 6–10 detik, jika customersInStore < seats + 1, spawn pelanggan baru.


7. Alur craft client ↔ server

Client                                   Server
──────                                   ──────
[E] Take Order ───── TakeOrder(orderId) ─► validasi: order ON_WAITING_TABLE di plot pemain,
                                           bench kosong β†’ state TAKEN_BY_PLAYER
            ◄──── OrderTaken(order) ─────
StartCraft(orderId) ─────────────────────► server mulai menghitung activeSec (os.clock)
(mini-game langkah demi langkah, lokal)
setiap langkah selesai:
  StepDone(orderId, step, choice) ───────► validasi:
                                           - step = langkah berikutnya yang benar
                                           - choice sudah dimiliki (warna/pola/stiker/bentuk)
                                           - jeda sejak langkah sebelumnya β‰₯ Steps[step].minSec Γ— faktor gamepass
                                           simpan ke order.progress
  StepDone langkah TERAKHIR ─────────────► pesanan selesai (tanpa DELIVER):
                                           hentikan timer, cek request (bonus 25%),
                                           activeSec ≀ targetSec (bonus 20%),
                                           hitung cash & XP (Formulas.luau), bayar,
                                           simpan recipe ke Album, pelanggan PICKUP
            ◄──── Reward(payload) ───────  rincian: gaji dasar, bonus sesuai, bonus cepat, bonus sosial
  • Client tidak pernah mengirim jumlah cash/XP. Server menghitung dari order.baseSalary, pilihan yang tervalidasi, dan waktu yang diukur server. Client tidak mengirim durasi, jadi bonus kecepatan tidak bisa dipalsukan.
  • durationSec minimum mencegah auto-clicker menyelesaikan pesanan dalam 1 detik. Jika gagal validasi, langkah ditolak (bukan kick). Catat ke analytics exploit_suspect.
  • Progres pesanan di bench disimpan di server, jadi PUT DOWN / disconnect tidak kehilangan pesanan selama sesi. Timer server berhenti saat PUT DOWN dan lanjut saat Resume.

Implementasi mini-game (client)

LangkahTeknik
ShapeList bentuk di tray HUD (filter: device pesanan + sudah terbuka). Tap β†’ StepDone(shape, shapeId) β†’ case polos di-spawn dengan tween bounce. Tanpa mini-game
Paint / SmoothingRaycast dari layar ke MeshPart case β†’ posisi lokal di bidang belakang β†’ indeks sel grid 8Γ—16. Visual: SurfaceGui dengan 128 Frame (warna/transparansi per sel). Setelah selesai, SurfaceGui dihapus dan Color/Material case diset
GlossyDeteksi 3 drag vertikal panjang (β‰₯ 60% tinggi case)
Pattern (glitter)Hitung goyangan (perubahan arah drag / tap), +8% per goyangan. Decal pola fade-in
Pattern (roller)3 drag horizontal, UIGradient sebagai mask reveal pada decal
StickerDecal/ImageLabel di SurfaceGui pada sisi belakang. Posisi u,v di-clamp keluar dari area kamera (Devices[d].cameraRect)
Clean4–6 noda sebagai ImageLabel. Setiap noda punya HP 1–2. Drag di atas noda mengurangi HP

8. CaseBuilder (shared)

CaseBuilder.build(recipe, lod) β†’ Model.

LODDipakai diIsi
fullBench, turntable, Gallery appMesh case + warna + finish + pola + stiker + partikel glitter
mediumDisplay rack, tangan avatar, bingkai Gallery SquareTanpa partikel
lowToko pemain lain yang jauh, Hall of Fame jauhMesh + warna saja

Cache Model per hash recipe di client supaya rak yang sama tidak dibangun berulang.


9. Remotes

RemoteArahPayloadRate limit
TakeOrderC→SorderId2/dtk
StartCraftC→SorderId2/dtk
StepDoneC→SorderId, step, choice5/dtk
PutDown / ResumeC→SorderId2/dtk
StopThiefC→SorderId5/dtk
RichKidHaggle / RichKidAcceptC→SorderId2/dtk
BuildTycoonC→StycoonId2/dtk
SetDecor / BuyDecorC→Sslot, variantId4/dtk
EmployeeUpgradeC→SempIndex, kind2/dtk
SetDisplay / EquipCaseC→Sslot, albumIndex4/dtk
CollectTipJarC→S—1/dtk
LikeC→StargetType, targetId2/dtk
GallerySubmitC→SalbumIndex1/10dtk
RedeemCodeCβ†’Sstring (≀ 20 char)1/2dtk
UseGemsC→Saction (instant/rush/boost/premium), targetId2/dtk
PlotStateSβ†’Csnapshot/delta: pelanggan, order, karyawan4–10 Hz delta
PlayerStateS→Ccash, gems, xp, level, bonussaat berubah
Reward, LevelUp, Toast, ServerFeedS→Cpayload UI—

Semua payload C→S divalidasi dengan t dan rate limit per pemain. Pelanggaran → abaikan + log.


10. Plot

  • 8 plot sebagai Model di Workspace dengan CFrame tetap. Saat pemain join β†’ plot kosong pertama, bangun ulang toko dari PlayerData.tycoon/decor.
  • Saat pemain keluar β†’ simpan data, kosongkan plot (animasi tutup toko di client lain).
  • Item tycoon/dekor = template di ServerStorage.Templates, di-clone ke slot plot.
  • Hologram hanya dibuat di client pemilik (pemain lain tidak perlu melihat hologram orang lain).

11. AFK

function AfkService.tick(data, now)
  local perMin = Formulas.displayIncomePerMin(data)          -- Ξ£ nilai_case Γ— 0.02
  local cap = perMin * (owns(NightShift) and 480 or 180)
  local mins = (now - data.tipJar.lastTick) / 60
  data.tipJar.amount = math.min(cap, data.tipJar.amount + perMin * mins)
  data.tipJar.lastTick = now
end

Dipanggil saat join (menghitung offline), setiap 30 detik saat online, dan saat isi rak berubah (supaya rate lama dihitung dulu). lastTick memakai os.time() server.

DataPenyimpanan
Entri minggu ini (recipe + pembuat)DataStore Gallery_<week> key = userId
Jumlah like per entriOrderedDataStore GalleryLikes_<week> key = userId. Update dengan UpdateAsync (increment), dibatch per server setiap 30 detik
Pool entri acak untuk app GalleryMemoryStore SortedMap GalleryPool_<week> (key = userId, value = recipe ringkas), TTL 8 hari
Siapa sudah like apaMemoryStore HashMap GalleryVoted_<week>_<voterId>, juga di profile likesGivenToday
Hall of FameDihitung server saat minggu berganti: GetSortedAsync(false, 8) β†’ simpan ke DataStore HallOfFame_<week>
HadiahSaat login, pemain cek peringkatnya di minggu lalu (GetSortedAsync halaman + cache) β†’ pendingRewards

week = format YYYYWW (ISO week, UTC).

13. Monetisasi & ProcessReceipt

MarketplaceService.ProcessReceipt = function(info)
  local profile = DataService.waitForProfile(info.PlayerId)
  if not profile then return Enum.ProductPurchaseDecision.NotProcessedYet end
  if profile.Data.receipts[info.PurchaseId] then
    return Enum.ProductPurchaseDecision.PurchaseGranted          -- sudah diberikan
  end
  local ok = Products.grant(profile, info.ProductId)             -- gems / boost / party / starter
  if not ok then return Enum.ProductPurchaseDecision.NotProcessedYet end
  profile.Data.receipts[info.PurchaseId] = true
  trimReceipts(profile.Data.receipts, 100)
  -- simpan segera supaya grant tidak hilang
  profile:Save()
  return Enum.ProductPurchaseDecision.PurchaseGranted
end
  • Gamepass dicek dengan UserOwnsGamePassAsync saat join (cache) + PromptGamePassPurchaseFinished untuk aktivasi langsung.
  • Cash Party disimpan sebagai boost server di memori server (partyEndsAt). Pemain yang join belakangan juga mendapat efek sampai waktu habis.

14. Analytics

Pakai AnalyticsService Roblox:

JenisEvent
Funnel FTUE (LogOnboardingFunnelStepEvent)1 spawn β†’ 2 take_first_order β†’ 3 first_paint β†’ 4 first_order_complete β†’ 5 level2 β†’ 6 build_pattern_shelf β†’ 7 level3 β†’ 8 hire_employee β†’ 9 level5 β†’ 10 display_first_case
Progression (LogProgressionEvent)level_up (level, menit main)
Economy (LogEconomyEvent)Setiap source/sink cash & gems dengan itemSku (contoh tycoon.glossyShelf, order.fancy)
Custom (LogCustomEvent)order_complete (bonus_match, bonus_speed, activeSec, targetSec, langkah, device, customerType), thief_result, haggle_result, gallery_submit, like_given, visit_shop, instant_finish, exploit_suspect

15. Performa & batas

ItemTarget
FPSβ‰₯ 30 di Android low-end (2 GB RAM), β‰₯ 60 di PC
Memori client≀ 900 MB di mobile
Server heartbeatβ‰₯ 55 Hz dengan 8 plot penuh
NPCModel NPC hanya di client. AnimationController + jalur waypoint tetap (tanpa PathfindingService). Plot > 120 stud: NPC disembunyikan
Replikasi plotDelta state 4–10 Hz, hanya untuk plot dalam radius streaming
DataStoreAuto-save ProfileStore (default) + save segera setelah pembelian Robux

16. Testing

TesCara
FormulasUnit test: cocokkan contoh hitungan di 08
Paritas simulasiScript test memanggil Formulas dengan input yang sama seperti tools/economy_sim.py, hasil harus identik
DispatchUnit test skenario: karyawan sibuk, skill kurang, table penuh, backlog
Order generator10.000 order per level β†’ cek bobot tipe/device dan bahwa semua request bisa dipenuhi
ExploitKirim StepDone beruntun lebih cepat dari minSec, choice belum dimiliki, order milik plot lain β†’ semua ditolak
Multi-deviceUji di HP Android murah, iPhone, PC, gamepad
LoadServer 8 pemain bot (Studio "Start Server, 8 players") 30 menit
Built with LogoFlowershow