# Porting CAFÉ CALM to Roblox Studio The three.js game was written so the Roblox port is mostly assembly, not a rewrite. Gameplay is deterministic and DOM-free, the tuning data is one small file, and the dangerous parts (fixed-step logic, hazard director, economy, events) map one-to-one onto server-side Luau. A starter Rojo tree lives in [`roblox/`](./roblox/). It compiles cleanly with the Luau CLI and includes a playable greybox, but it has **not** been run in Roblox Studio in this environment. Treat it as a strong first commit for the Studio port, not a finished place. --- ## 1. The invariants that make the port easy Four rules are worth preserving when you edit the web version: 1. **`src/shared/session.js` never touches the DOM or three.js.** It takes state + input + `dt`, returns state + events. The Luau port only has to mirror maths, not APIs. 2. **All tuning is data.** `src/shared/config.js` mirrors `roblox/src/shared/Config.luau`. Hazard timings, weights, damage, arena bounds and scoring are not scattered across render code. 3. **The simulation ticks at 60 Hz.** Roblox runs its own heartbeat; clamp `dt` to `0.05` and step a fixed accumulator (or run the server at a fixed 1/60 interval) so the same timings hold. 4. **Events are one-way.** The web simulation emits `sip`, `aura_up`, `hit`, `flare_burst`, `act`, `end` etc.; the renderer/HUD/audio consume them. Roblox sends the same event shape to the client over a RemoteEvent. ## 2. File map | Browser file | Roblox equivalent | Notes | | --- | --- | --- | | `src/shared/config.js` | `roblox/src/shared/Config.luau` | data-only, parity checked | | `src/shared/session.js` | `roblox/src/shared/GameRules.luau` + `Main.server.luau` | Lua mirrors economy; server owns movement/hazards | | `src/shared/rng.js` | `Random.new(seed)` | server picks the seed and owns the director | | `src/web/world.js` | `CafeBuilder.luau` + Studio art pass | greybox in the starter | | `src/web/rig.js` | real R6 avatar / `R6Rig.luau` | players should use R6 | | `src/web/props.js` | Studio models/Part prototypes | tables, broom, baguette etc. | | `src/web/hud.js` + `styles.css` | `Hud.client.luau` | code-built ScreenGui in the starter | | `src/web/audio.js` | `SoundService` / `SoundGroup` | not ported yet | | `src/web/input.js` | Roblox Humanoid + `ProximityPrompt` | jump unsits, prompt sits | | `src/web/engine.js` | Roblox camera + Lighting | default camera in the starter | | `tools/luau-check.mjs` | CI/syntax check | compiles all `.luau` | | `tools/luau-parity.mjs` | config parity check | compares web and Luau numbers | ## 3. Units and coordinates - 1 browser unit = 1 Roblox stud. - `x` is right, `z` is toward the browser camera (south), `y` is up. - The café wall is on the west/left (`x ≈ -18`), the street barricade is on the north/back (`z ≈ -12`), and gameplay is clamped to `x ∈ [-16.6, 16.6]`, `z ∈ [-10.7, 10.7]`. - R6 avatars are roughly 5 studs tall. The browser avatar is ~4.7 units; the camera expects a slightly taller Roblox character. Table heights and interaction distances are already in stud-like units. - Browser Y is feet height. Roblox `HumanoidRootPart.Position.Y` is the torso centre. For ground-based distances use `X/Z`; for jump clearance use `humanoid.FloorMaterial == Enum.Material.Air` (as the starter does), not the raw root Y. ## 4. Server authority The browser is single-player and trusts its own simulation. A Roblox place must not: - let the client award views/AURA, - let the client decide a hazard hit, - accept an arbitrary table ID from the client. In the starter: - `GameRules.luau` is required by the server only. - The server owns `activeSession`, the director timer and hit detection. - The server sends a read-only `State` snapshot and a one-way `Event` stream to the client at ~10 Hz. - `ProximityPrompt.Triggered` calls `Seat:Sit(humanoid)` on the server. - The client fires `Retake` after an ended round; the server validates that the round is actually over before starting a new one. If you add multiplayer, use one session table per player, one director per session, and one RemoteEvent per player. Do not share calm/views between players. The current starter intentionally supports solo takes. ## 5. Economy mapping Keep these in sync between `src/shared/config.js` and `Config.luau`: | Rule | Browser | Luau starter | | --- | --- | --- | | Seated views | `18 + aura × 9` per second | `GameRules.stepEconomy` | | B-roll while standing | `2.5` views/s | same | | AURA step | every `3.0s` while seated, max 10 | same | | Sip | every `2.4s`, `+38 × aura` views, `+3` calm | same | | Calm regen while seated | `2.4/s` | same | | Hit | AURA resets to 1; seated damage ×1.25 | same | | Near miss | `14 × aura` views | `GameRules.nearMiss` | | Croissant | +20 calm, +90 views | not spawned in the starter | | Ranks | 0 / 2,500 / 6,000 / 10,000 / 15,000 views | same | The web's calm regen was retuned from 7.0 to 2.4 after a balance pass: with the old value, sitting still and ignoring every hazard was the optimal (and unkillable) strategy. The starter hard-codes the same 2.4. `src/shared/session.js` also handles movement, jump and collision; in the Roblox starter those are delegated to the Roblox Humanoid and to `HazardService.luau`. That is the intended split: **GameRules owns scoring, Main/HazardService own the world.** ## 6. Hazard director The web weights, per act: | Kind | Telegraph | Damage | Radius | Speed | Low / jumpable | | --- | --- | --- | --- | --- | --- | | flare | 1.15 s | 22 | 2.75 | — | no | | baguette | 1.05 s | 11 | 0.72 | 15.5 | yes | | scooter | 1.20 s | 17 | 1.00 | 13.5 | yes | | pigeon | 1.00 s | 8 | 1.55 | — | no | Weights per act (I/II/III): | Kind | I | II | III | | --- | --- | --- | --- | | flare | 0.38 | 0.30 | 0.26 | | baguette | 0.62 | 0.25 | 0.20 | | scooter | 0 | 0.25 | 0.25 | | pigeon | 0 | 0.20 | 0.29 | Spawn interval ramps from `3.35s` to `2.05s` with ±0.38s jitter. The first hazard lands at `2.4s`; maximum concurrent hazards is capped at 6. All of these live in `Config` / `GameRules.pickHazard`. The starter's `HazardService.luau` is a greybox implementation: - Flare: ground ring + marker, then a neon blast and point light. - Baguette/scooter: warning lane across the arena, then a part that sweeps in from the side and damages on proximity. - Pigeon: warning ring and three bird parts. - Markers are BillboardGuis with the same action copy as the web HUD. - Jumpable hazards check `humanoid.FloorMaterial == Enum.Material.Air`. Replace the Parts with meshes/attachments but keep the timings and radii. For polish, recreate the web's hazard markers, lane decals, pulsing materials and screen shake in a Studio art pass. ## 7. Sitting and the meme loop The browser uses a custom `E` toggle and auto-stands when you try to walk. Roblox should embrace native behaviour: - Each chair is a real `Seat` Part with a `ProximityPrompt` (`SIT & SIP`). - `Seat:Sit(humanoid)` puts the character in the chair. - Jumping unsits the character (Space / mobile jump). - A seated player earns views/AURA and cannot dodge; leaving the seat resets the AURA decay timer, and a hit resets AURA to 1. - The server checks `humanoid.Sit and humanoid.SeatPart ~= nil` every tick. The starter auto-sits the player at the centre table when the round begins so the first frame matches the meme. You can remove that `task.delay` in `Main.server.luau` if you prefer players to find a seat themselves. ## 8. R6 avatars The browser avatar is built from boxes to look like a classic Roblox R6 character: yellow head and arms, blue torso, green legs, a red beret and a point-and-shoot camera. In Roblox, let the player use their real avatar, but set the place to **R6** for the intended silhouette. `R6Rig.luau` is for NPCs, not for overriding the player: - `Players:CreateHumanoidModelFromDescriptionAsync(desc, Enum.HumanoidRigType.R6)` gives a real R6 model with a Humanoid and Motor6Ds. - The helper adds the beret/camera/barista apron, a SurfaceGui face and a name tag. - `R6Rig.pose(rig, "seated" | "barista" | "idle")` rotates the R6 Motor6Ds. - The server calls it once during setup. Never call the yielding API inside a 60 Hz loop. The starter spawns `barista.baguette` near the café to demonstrate it. ## 9. HUD, input, camera, audio - HUD: `Hud.client.luau` builds a code-only ScreenGui with the cream/espresso language and the same AURA/Calm/Views/Act/End copy. It is intentionally a starter; re-author it in Studio with the web HUD screenshots as reference. - Input: Roblox's default Humanoid controls are the port's input layer. The browser's `E` seat prompt becomes a `ProximityPrompt`; the browser's mobile joystick becomes Roblox's built-in thumbstick. No custom controller is required. - Camera: the web game uses a fixed 3/4 camera aimed from `+Z` toward the café. For Roblox, first play with the default camera because seats and the ProximityPrompt are easier to understand. If you want the web framing, add a client `CameraScript` that follows `HumanoidRootPart` with an offset like `(0, 9, 18)` and looks at the torso, with a shorter offset on mobile. - Audio: not ported. The web soundtrack and SFX are synthesized with WebAudio; recreate them with a `SoundGroup` and Sound assets, or skip audio for the greybox. - Mobile: Roblox's default controls and ProximityPrompt are already usable. If you build a custom HUD, use `UIScale`/anchor points and keep the retake button above Roblox's jump button. ## 10. Studio art pass (suggested) Use `assets/shots/preview-gameplay.png` and `preview-menu.png` as the reference. The important reads are: 1. **Café wall + awning** on the left, cream plaster, dark green ground floor, red-and-cream stripes, `CAFÉ CALM` sign, `SIP HAPPENS.` blade sign. 2. **Warm sunset sky** with the Eiffel Tower silhouette and low Haussmann buildings. In Studio, use a Sky object, warm Lighting, Atmosphere and a distant tower mesh rather than hundreds of Part windows. 3. **Five bistro tables**, cream cloth, coffee cup, croissant, wicker chairs. 4. **Terrace cobbles**, planters, string lights and the low barricade. 5. **Protest crowd silhouettes** with coloured shirts, flags and the `NON AU BRUNCH FROID` banner. Keep them background-only and non-collidable. 6. **Rooftop gorilla** doing four poses of regret, plus its poster. This is the second meme cameo; it does not affect gameplay. 7. **Hazard shapes:** giant baguette, tiny teal scooter, pigeon flock, flare canister and expanding ring. Make them read at a glance from the fixed camera. ## 11. Step-by-step port 1. Create a blank Studio place. Set Avatar = R6, MaxPlayers = 1, Streaming off. 2. Install Rojo; run `rojo serve` from `roblox/`; connect the plugin. 3. Press Play. Confirm the café builds, the prompt sits the player, hazards spawn, Calm drops and the end card/retake flow works. 4. Replace `CafeBuilder` greybox pieces with the Studio art pass, keeping the table/seat coordinates in `Config.Tables` or moving hazards to a shared arena reference if you re-layout. 5. Replace the greybox hazard Parts with meshes. Keep the `Config.Hazards` timings and radii; use `HazardService` as the state machine. 6. Rebuild the HUD in Studio with the web screenshots as reference. Keep the `State`/`Event`/`Retake` remote contract. 7. Add sound, a DataStore best and server-side validation for retakes and seat prompts. (The starter only validates round state, which is enough for a prototype.) 8. Playtest on mobile and low-end devices. The web's mobile quality tier already disables shadows and caps DPR; in Roblox, watch part count and transparent/decal overdraw. 9. Publish as a private place first. Check the Roblox guidelines and do not use official Roblox assets or imply endorsement. ## 12. Known gaps in the starter - No `.rbxl` file; Rojo source only. - Not run inside Roblox Studio here; syntax is validated, runtime is not. - Greybox art, not the web art. - No audio, DataStore, leaderboard, croissants, analytics or monetisation. - No multiplayer. The browser is a solo 90-second take; the starter is the same. - Hazard projectile direction uses `math.random`; seed it with `Random.new` for deterministic replays. - The web's custom 3/4 camera and auto-stand-on-movement are not ported. Jumping to stand is the Roblox-native equivalent. ## 13. Verification commands From the project root: ```bash npm test # JS simulation tests npm run check # JS syntax node tools/luau-check.mjs node tools/luau-parity.mjs ``` The parity check executes the pure Luau config/rules with the standalone Luau CLI and compares 19 tuning values plus a smoke test of the economy and ranks. ## Sources for the concept - Asianet Newsable, Oct 2026: [Woman calmly sips coffee as Paris protests rage behind her](https://newsable.asianetnews.com/world/woman-calmly-sips-coffee-at-paris-cafe-as-protests-and-flares-rage-behind-her-internet-makes-memes-articleshow-mgccq20) - News18, Oct 2026: [The heartbroken gorilla and its four poses of regret](https://www.news18.com/viral/bro-hit-the-four-poses-of-regret-heartbroken-gorilla-has-become-the-internets-most-relatable-husband-watch-10140429.html) - The Chive, Oct 2026: [Zero Context, Maximum Funny](https://thechive.com/funny/random-funny-best-memes-past-week-october-17-2026/) (weekly meme roundup consulted for this week's tone) All game names, characters and events are fictional. This is an unofficial fan concept, not affiliated with Roblox Corporation.