Some of the systems in Apocrypha, written in Luau. Each one has a short piece of the actual code.
NPC daily life
NPCs in town follow the in-game clock. They go to work, eat at the cantina, hang out or pick flowers in the evening and go home to sleep. The cantina keeper takes orders and serves real menu items, NPCs talk to each other, and the ones who drink too much start slurring. The code below is the slurring, plus the shuffle that stops NPCs repeating the same lines.
- About 8,000 lines
- Server
- Pathfinding
From NPCLife/Engine
-- slurred, once they've had a fewlocal function slur(text, level) if level < DRUNK then return text end local chance = level >= WASTED and 0.75 or 0.4 text = text:gsub("s([^h])", function(after) if math.random() < chance then return "sh" .. after end end) if math.random() < (level >= WASTED and 0.5 or 0.25) and not text:find("*hic*", 1, true) then text = text .. " *hic*" end return textend-- a shuffled deck: everything comes up once before anything comes up again (and never twice in a row)local Deck = {}Deck.__index = Deckfunction Deck.new(list) return setmetatable({list = list or {}, order = {}}, Deck)endfunction Deck:deal() if #self.order == 0 then for i = 1, #self.list do self.order[i] = i end for i = #self.order, 2, -1 do local j = math.random(i) self.order[i], self.order[j] = self.order[j], self.order[i] end if #self.order > 1 and self.order[#self.order] == self.last then self.order[1], self.order[#self.order] = self.order[#self.order], self.order[1] end end local index = table.remove(self.order) self.last = index return self.list[index] or ""end
Player shops
Players can buy a market stall, name it, stock it from their inventory and set their own prices. Each stall saves on its own and stays in sync across servers. Rent is due every week, and sales go into a till, so sellers still get paid while they’re offline. The code below handles a purchase, including the refund if someone else bought the item first.
- About 1,240 lines
- Server + client
- DataStores
From WorldServices/PlayerShops
function actions.purchase(plr, id, itemId) local r = booths[id].record if not r or not r.owner then return {ok = false, msg = "This stall is empty."} end if r.owner == plr.UserId then return {ok = false, msg = "That's your own stall."} end if owedWeeks(r) > 0 then return {ok = false, msg = "This shop is closed right now."} end local listed for _, it in ipairs(r.items or {}) do if it.id == itemId then listed = it end end if not listed then return {ok = false, msg = "That item was just sold."} end local c = credits(plr) if not c or c.Value < listed.price then return {ok = false, msg = "You don't have enough credits."} end c.Value -= listed.price local removed local ok, record = updateBooth(id, function(old) removed = nil if not old or old.owner ~= r.owner or owedWeeks(old) > 0 then return nil end for i, it in ipairs(old.items or {}) do if it.id == itemId and it.price == listed.price then removed = table.remove(old.items, i) old.till = (old.till or 0) + it.price return old end end return nil end) if not ok or not record or not removed then c.Value += listed.price if ok then reload(id) end return {ok = false, msg = "That item was just sold or changed. You weren't charged."} end setRecord(id, record, true) if not giveItem(plr, removed) then warn("[PlayerShops] couldn't hand over " .. removed.kind .. " " .. removed.name .. " to " .. plr.Name) end return {ok = true, msg = "Bought " .. removed.name .. ".", view = browseView(id)}end
Crash-site tutorial
The first few minutes for a new character: look around, get to the wreck, sprint, jump, search a supply crate, follow a trail out of the valley and talk to Timothy in town. Progress saves, so if you rejoin you pick up where you left off. The code below is the supply crate. The server only pays out once per character, and only if you’re actually standing next to it.
- About 760 lines
- Server + client
From WorldServices/CrashTutorial
-- the supply crate: once per character, only during the tutorial, only within reachlocal busy = {}local function onSearch(player) if busy[player] then return end busy[player] = true local folder = playerFolderOf(player) local list = folder and readList(folder) local points = sitePoints() local root = rootOf(player) if folder and list and points and points.crate and root and (root.Position - points.crate).Magnitude <= CRATE_REACH then local paid = 0 if not table.find(list, "CrashSupplies") then paid = SUPPLY_CREDITS local credits = folder:FindFirstChild("Credits") if credits then credits.Value += paid end addFlag(folder, list, "CrashSupplies") end notify:FireClient(player, "searched", paid) end task.delay(1, function() busy[player] = nil end)end
Mustafar lava effects
All the lava planet effects: bubbles that pop, a rumble that gets louder near lava, ash during storms, and eruptions with screen shake, lava bombs and a smoke column. The server just says when an eruption starts, and each player’s game plays it out, so if you show up halfway through you still see the rest. The code below is the eruption timeline.
- About 680 lines
- Client + server
From MustafarFX
local function once(key, at, t, fn) -- run fn when the timeline passes `at`, unless we joined well after it local e = eruption if not e.done[key] and t >= at then e.done[key] = true if e.joinedAt <= at + 3 then fn() end endendlocal function stepEruption(dt, loudness) local e = eruption local t = os.clock() - e.t0 local power = e.power -- shake: builds for 2 s, goes wild for 7, dies away over 15 local env if t < 2 then env = 0.35 * (t / 2) elseif t < 9 then env = 1 elseif t < 24 then env = (1 - (t - 9) / 15) ^ 2 else env = 0 end local scale = player:GetAttribute("MustafarShakeScale") shakeAmp = env * power * loudness * ((typeof(scale) == "number") and math.clamp(scale, 0, 2) or 1) -- sounds once("quake", 0, t, function() quakeSound.Volume = 1.2 * loudness; quakeSound:Play() end) once("blast", 2, t, function() blastSound.Volume = 2.2 * loudness blastSound:Play() boomSound.Volume = 2.5 * loudness boomSound:Play() end) once("debris", 6, t, function() debrisSound.Volume = 0.9 * loudness; debrisSound:Play() end) once("boom2", 9.5, t, function() boomSound.Volume = 1.6 * loudness; boomSound:Play() end) if t > 22 and quakeSound.IsPlaying then quakeSound.Volume = math.max(quakeSound.Volume - dt * 0.25, 0) end -- the blast cloud, lava bombs and the smoke column once("ring", 2, t, function() for n = 1, 14 do local a = n / 14 * math.pi * 2 spawnPuff(crater, Vector3.new(math.cos(a) * 45, 70, math.sin(a) * 45), 180, 560, 26, 0) end end) once("bombs1", 2, t, function() launchBombs(16, power) end) once("bombs2", 5, t, function() launchBombs(8, power) end) once("bombs3", 9, t, function() launchBombs(8, power * 0.9) end) once("bombs4", 14, t, function() launchBombs(6, power * 0.8) end)
Map generator
The build script behind the Badlands south plain. It places rocks, dunes and sand on a 10-stud grid, copying real desert parts so the new ground matches the old, and it never overlaps anything that’s already there. The same seed gives the same map every time. The code below places the rock clusters.
- 574 lines
- Studio build script
From Tatooine_SouthFill_Generator
-- 2. rock clusters insidesetStatus("building rock clusters")local clusters, tries = {}, 0while #clusters < 34 and tries < 8000 do tries += 1 local ix, iz = rng:NextInteger(0, NX - 1), rng:NextInteger(0, NZ - 1) local i = idx(ix, iz) if buffer.readu8(bare, i) == 1 and buffer.readu8(nearB, i) == 0 and buffer.readu8(rockCover, i) == 0 and dv(ix, iz) >= 26 then local x, z = centre(ix, iz) if townDist(x, z) > 170 and corrDist(x, z) > 50 then local spaced = true for _, c in ipairs(clusters) do if (c[1] - x) ^ 2 + (c[2] - z) ^ 2 < 330 ^ 2 then spaced = false break end end if spaced then clusters[#clusters + 1] = {x, z} end end end if tries % 300 == 0 then breathe() endendlocal rockList = {}for _, c in ipairs(clusters) do local yaw0 = yawPick() local W, D, H = rng:NextNumber(70, 160), rng:NextNumber(60, 130), rng:NextNumber(80, 250) local ux, uz = math.cos(math.rad(yaw0)), math.sin(math.rad(yaw0)) local core = placeRock(F.Rock, "Rock", c[1], c[2], W, D, H, ux, uz, nearB, false, 120, 20) if core then counts.rock += 1 rockList[#rockList + 1] = core for _ = 1, rng:NextInteger(2, 6) do local a = rng:NextNumber(0, 2 * math.pi) local w, d = rng:NextNumber(40, 120), rng:NextNumber(40, 110) local r = (math.max(W, D) / 2 + math.max(w, d) / 2) * rng:NextNumber(0.45, 0.9) local sux, suz = rot(ux, uz, rng:NextNumber(-8, 8)) local s = placeRock(F.Rock, "Rock", c[1] + math.cos(a) * r, c[2] + math.sin(a) * r, w, d, H * rng:NextNumber(0.2, 0.75), sux, suz, nearB, false, 120, 20) if s then counts.rock += 1 rockList[#rockList + 1] = s end end end breathe()end