14. Dokumen Teknis Programmer
14. Dokumen Teknis Programmer
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
| # | Keputusan | Alasan |
|---|---|---|
| 1 | Case 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 |
| 2 | Tanpa EditableImage di V1. Painting memakai grid sel + SurfaceGui sementara, hasil akhir = warna solid + decal | EditableImage masih beta, dibatasi memori, dan mewajibkan verifikasi umur/ID [SRC Analisis S23]. Free-paint ditunda |
| 3 | Server-authoritative untuk ekonomi. Client hanya mengirim pilihan tiap langkah. Server memvalidasi, mengukur waktu kerja sendiri, menghitung bonus, cash, dan XP | Mencegah exploit cash/XP |
| 4 | Semua angka di ModuleScript config (ReplicatedStorage.Shared.Config) | Prinsip game sumber: semua angka di tabel [SRC 08]. Siap dipindah ke remote config di V1.1 |
| 5 | NPC divisualkan di client, logika di server | 8 plot Γ Β±10 NPC = Β±80 NPC. Server hanya menyimpan state ringan, client merender model + animasi untuk plot yang dekat |
| 6 | ProfileStore untuk data pemain | Session locking, auto-save, aman dari data hilang/duplikasi |
| 7 | StreamingEnabled aktif | Device mobile low-end |
2. Stack & tooling
| Bagian | Pilihan |
|---|---|
| Bahasa | Luau --!strict |
| Sinkronisasi file | Rojo + Git |
| Package | Wally: ProfileStore, Signal (GoodSignal), Promise, t (validasi tipe remote) |
| UI | React-Lua (jsdotlua/react) atau Fusion. Pilih satu di awal dan jangan dicampur |
| Test | Jest-Lua / TestEZ untuk modul murni (rumus ekonomi, dispatch, order generator) |
| Lint | Selene + 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
| Modul | Isi | Sumber |
|---|---|---|
Levels | xpToNext, unlock per level (customer, device, decor, tycoon, colors, patterns, stickers, shapes), gems level-up | 07 |
Devices | id, nama tampil, baseCash, timeFactor, daftar shapes | 06 |
Steps | id, cash, unlockLevel, minDurationSec, kata konfirmasi, skill karyawan | 04, 08 |
Customers | id, cashMult, xpMult, bobot per level, jumlah elemen request, aturan khusus | 06 |
Colors / Patterns / Stickers / Shapes | id, nama, hex/aset, unlockLevel, premiumGems? | 07, 15 |
Tycoon | id, level, harga, efek | 05 |
Decor | slot, varian (Basic/Cute/Premium), harga, level | 05 |
Employees | harga rekrut, skill & biaya, tabel Speed & Quality | 05 |
Economy | bonus kesesuaian (0,25) & kecepatan (0,20), pengali target L1β2 (1,5), bonus sosial, AFK rate & cap, harga Instant Finish/Rush, login, quest | 08 |
Monetization | GamePassId, ProductId, isi produk | 09 |
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. durationSecminimum mencegah auto-clicker menyelesaikan pesanan dalam 1 detik. Jika gagal validasi, langkah ditolak (bukan kick). Catat ke analyticsexploit_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)
| Langkah | Teknik |
|---|---|
| Shape | List bentuk di tray HUD (filter: device pesanan + sudah terbuka). Tap β StepDone(shape, shapeId) β case polos di-spawn dengan tween bounce. Tanpa mini-game |
| Paint / Smoothing | Raycast 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 |
| Glossy | Deteksi 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 |
| Sticker | Decal/ImageLabel di SurfaceGui pada sisi belakang. Posisi u,v di-clamp keluar dari area kamera (Devices[d].cameraRect) |
| Clean | 4β6 noda sebagai ImageLabel. Setiap noda punya HP 1β2. Drag di atas noda mengurangi HP |
8. CaseBuilder (shared)
CaseBuilder.build(recipe, lod) β Model.
| LOD | Dipakai di | Isi |
|---|---|---|
full | Bench, turntable, Gallery app | Mesh case + warna + finish + pola + stiker + partikel glitter |
medium | Display rack, tangan avatar, bingkai Gallery Square | Tanpa partikel |
low | Toko pemain lain yang jauh, Hall of Fame jauh | Mesh + warna saja |
Cache Model per hash recipe di client supaya rak yang sama tidak dibangun berulang.
9. Remotes
| Remote | Arah | Payload | Rate limit |
|---|---|---|---|
TakeOrder | CβS | orderId | 2/dtk |
StartCraft | CβS | orderId | 2/dtk |
StepDone | CβS | orderId, step, choice | 5/dtk |
PutDown / Resume | CβS | orderId | 2/dtk |
StopThief | CβS | orderId | 5/dtk |
RichKidHaggle / RichKidAccept | CβS | orderId | 2/dtk |
BuildTycoon | CβS | tycoonId | 2/dtk |
SetDecor / BuyDecor | CβS | slot, variantId | 4/dtk |
EmployeeUpgrade | CβS | empIndex, kind | 2/dtk |
SetDisplay / EquipCase | CβS | slot, albumIndex | 4/dtk |
CollectTipJar | CβS | β | 1/dtk |
Like | CβS | targetType, targetId | 2/dtk |
GallerySubmit | CβS | albumIndex | 1/10dtk |
RedeemCode | CβS | string (β€ 20 char) | 1/2dtk |
UseGems | CβS | action (instant/rush/boost/premium), targetId | 2/dtk |
PlotState | SβC | snapshot/delta: pelanggan, order, karyawan | 4β10 Hz delta |
PlayerState | SβC | cash, gems, xp, level, bonus | saat berubah |
Reward, LevelUp, Toast, ServerFeed | SβC | payload 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
CFrametetap. Saat pemain join β plot kosong pertama, bangun ulang toko dariPlayerData.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.
12. Gallery (Best Case of the Week)
| Data | Penyimpanan |
|---|---|
| Entri minggu ini (recipe + pembuat) | DataStore Gallery_<week> key = userId |
| Jumlah like per entri | OrderedDataStore GalleryLikes_<week> key = userId. Update dengan UpdateAsync (increment), dibatch per server setiap 30 detik |
| Pool entri acak untuk app Gallery | MemoryStore SortedMap GalleryPool_<week> (key = userId, value = recipe ringkas), TTL 8 hari |
| Siapa sudah like apa | MemoryStore HashMap GalleryVoted_<week>_<voterId>, juga di profile likesGivenToday |
| Hall of Fame | Dihitung server saat minggu berganti: GetSortedAsync(false, 8) β simpan ke DataStore HallOfFame_<week> |
| Hadiah | Saat 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
UserOwnsGamePassAsyncsaat join (cache) +PromptGamePassPurchaseFinisheduntuk 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:
| Jenis | Event |
|---|---|
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
| Item | Target |
|---|---|
| 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 |
| NPC | Model NPC hanya di client. AnimationController + jalur waypoint tetap (tanpa PathfindingService). Plot > 120 stud: NPC disembunyikan |
| Replikasi plot | Delta state 4β10 Hz, hanya untuk plot dalam radius streaming |
| DataStore | Auto-save ProfileStore (default) + save segera setelah pembelian Robux |
16. Testing
| Tes | Cara |
|---|---|
Formulas | Unit test: cocokkan contoh hitungan di 08 |
| Paritas simulasi | Script test memanggil Formulas dengan input yang sama seperti tools/economy_sim.py, hasil harus identik |
| Dispatch | Unit test skenario: karyawan sibuk, skill kurang, table penuh, backlog |
| Order generator | 10.000 order per level β cek bobot tipe/device dan bahwa semua request bisa dipenuhi |
| Exploit | Kirim StepDone beruntun lebih cepat dari minSec, choice belum dimiliki, order milik plot lain β semua ditolak |
| Multi-device | Uji di HP Android murah, iPhone, PC, gamepad |
| Load | Server 8 pemain bot (Studio "Start Server, 8 players") 30 menit |