# Porting BOO TAG to Roblox Studio This project was built so the **rules** are engine-agnostic and the **presentation** is replaceable. The web build is the playable reference; `roblox/` is a Rojo project with the shared rules already translated to Luau, plus a working server/client skeleton. The short version: **keep `src/shared` and `src/server` as the source of truth, delete `src/client`, and let Roblox supply the renderer, avatars, camera and input.** --- ## 1. What maps to what | BOO TAG web | Roblox Studio | Notes | | --- | --- | --- | | `src/shared/config.js` | `ReplicatedStorage/BooTag/Config.luau` | Tunables, ranks, map layout, palettes | | `src/shared/scoring.js` | `ReplicatedStorage/BooTag/Scoring.luau` | Pure functions; already translated | | `src/shared/ai.js` | `ReplicatedStorage/BooTag/BotBrain.luau` | NPC state machine; already translated | | `src/shared/collision.js` | Roblox collision / `Workspace` parts | Let Roblox do the physics | | `src/shared/protocol.js` | `ReplicatedStorage/BooTag/Remotes.luau` | Event names become RemoteEvents | | `src/server/simulation.js` | `ServerScriptService/BooTagServer.server.luau` | Round, NPCs, validation | | `src/client/main.js` | `StarterPlayerScripts/BooTagClient.client.luau` | Wiring only | | `src/client/player.js` | Roblox `Humanoid` + `Camera` | Delete the custom controller | | `src/client/avatar.js` | `StarterCharacter` / R6 rig | Roblox already gives you an R6 character | | `src/client/world.js` | `Workspace/SpookBlock` | Build from `Config.Layout.Houses` | | `src/client/textures.js` | `Decal` / `SurfaceGui` / `ImageLabel` | Or keep generating via `EditableImage` | | `src/client/hud.js` + `index.html` + `styles.css` | `ScreenGui` | Rebuild frames in Studio | | `src/client/audio.js` | `SoundService` + `Sound` | Replace synth with SoundIds | | `src/client/fx.js` | `ParticleEmitter` / `Attachment` | Optional polish | | `src/client/touch.js` | Roblox mobile controls | Delete; Roblox provides them | | `window.__bootag` hooks | `_G.BooTagDebug` / test place | Strip before publishing | ### Units and coordinates - 1 web unit = **1 Roblox stud**. All speeds/ranges in `Config` are already studs and seconds. - The web rig is **R6-proportioned**: torso 2×2×1, head 1.25³, arms/legs 1×2×1, feet at `y=0`. A default Roblox R6 character is 5 studs tall, close enough for the same camera feel. - Web `yaw` is `rotation.y`, where `0` faces **+Z**. Roblox `CFrame.lookAt` with a forward of `(sin(yaw), 0, cos(yaw))` reproduces it exactly. - The map layout is authored around the origin; `Config.Layout.Spawn` is `(0, 0, 9)`. --- ## 2. The server contract The server owns everything that can affect clout. The client only sends intents. ### RemoteEvents (Server → Client) | Event | Payload | Purpose | | --- | --- | --- | | `Snapshot` | `{ serverTime, npcs = { {id, name, palette, x, z, yaw, state, nerves, speed, moving} } }` | Replicate NPC poses/state | | `State` | `{ phase, timeLeft, countdown, clout, combo, multiplier, comboLeft, hype, frenzy, frenzyLeft, scares, caught }` | HUD state | | `ScareResult` | `{ ok, npcId, points, clean, multiplier, frenzy, frenzyStarted, x, z, name, clout }` | Celebrate a boo | | `Caught` | `{ penalty, npcId, npcName, clout }` | Player got tagged | | `Toast` | `{ text, kind }` | Feed message | | `Round` | `{ phase }` | `countdown` / `live` / `ended` | | `End` | full `State` + `rank`, `bestCombo`, `scares`, `cleanScares`, `caught`, `frenzies` | Results | | `Fx` | `{ kind, x, z }` | Optional client particles | ### RemoteEvents (Client → Server) | Event | Payload | Validation | | --- | --- | --- | | `Start` | none | Starts countdown if not already counting down | | `Again` | none | Same as Start from the results screen | | `Transform` | `{ sneaking, sprinting, moving }` | Optional; use `Humanoid.MoveDirection` instead | | `Scare` | `{ npcId }` | Server checks phase, range, cooldown, state, then calls `Scoring.awardScare` | The web `Transform` event also carries `x/z/yaw`, because the server has no Humanoid. In Roblox you can ignore those fields and read `HumanoidRootPart.Position`, which is already server-visible (and is the more authoritative version of the same design). ### The rule that must not be skipped `Scare` must be validated on the server: ```lua local dist = (Vector2.new(playerPos.X, playerPos.Z) - Vector2.new(npc.x, npc.z)).Magnitude if dist > Config.Scare.Range + 0.75 then return end local clean = BotBrain.isBehind(npc, playerPos) and npc.state ~= "alert" local result = Scoring.awardScare(score, clean, now) BotBrain.spook(npc, now) ``` If you trust the client, the entire game becomes a `RemoteEvent` spam simulator. This is exactly the same check the browser build runs in `src/server/simulation.js`. --- ## 3. Characters The web build generates an R6-style rig from boxes: | Group | Roblox equivalent | | --- | --- | | `group` | `Model` + `HumanoidRootPart` | | `body` | `Torso` | | `head` + face texture | `Head` + face `Decal` on the front | | `armL/armR` | `Left Arm` / `Right Arm`, pivot at the shoulder | | `legL/legR` | `Left Leg` / `Right Leg`, pivot at the hip | | `nameplate` sprite | `BillboardGui` + `TextLabel` | | ring under player | selection `Part` / `Highlight` | For NPCs, the Luau server builds a single anchored `Model` per NPC and `PivotTo`s it. If you want the walk cycle, add `Motor6D` joints and drive `Motor6D.Transform` with the same phase offsets used by `src/client/avatar.js`: - walk: legs `±0.55`, arms `∓0.42` - sprint: `±0.85`, body lean `0.18` - tiptoe: arms out on Z (`±1.42`), knees up `0.85` - scare: arms up (`-2.75`), head tilt `0.18` - chase: arms up and flailing, body lean `0.24` The player should use a normal Roblox R6 character with the default Animate script; only the **sneak** pose needs custom work (set `Humanoid.WalkSpeed = 7` and optionally play a tiptoe animation). NPC palettes are in `Config.Palettes` and map directly to `BrickColor`/`Color3` values. The names (`Bacon_Hair_2012`, `Guest1337`, `Malfoid`, `Rumpel`, …) are deliberately generic archetypes, not copies of Roblox's catalog assets. --- ## 4. Spook Block `Config.Layout` already lists the houses, lamps, trees and spawn: ```lua Houses = { { id = "haunted", x = 0, z = -39, w = 22, d = 13, h = 10, haunted = true }, { id = "west", x = -29, z = -27, w = 17, d = 13, h = 8.5 }, ... } ``` In Studio: 1. Make a `Folder` called `SpookBlock` in `Workspace`. 2. For each house, create a `Part` at `(x, h/2 + 0.6, z)` sized `(w, h, d)`, plus a `WedgePart`/roof and a door facing the origin. 3. Add the cat statue at `(11.5, 0, 2.6)` (the web build's "The Clapper"). Its two hand parts are what the client animates on a scare. 4. Put a `BillboardGui` or `SurfaceGui` with "BOO!" at `(0, 9.6, 36)` facing the plaza. 5. Lamps are in `Config.Layout.Lamps`; use a `PointLight` with a warm colour (`#ffc46b`) and a low intensity so the night reads. 6. Keep the playable area inside radius **40 studs** so the arena clamp in `Config` matches. The web build also paints a giant "BOO" decal on the ground at `(0, 0, -2)`; a `Decal` or `SurfaceGui` on a flat part is enough. --- ## 5. UI The web HUD is three DOM layers: the HUD, the prompt, and the Boo Cam. In Studio: - `ScreenGui` → `CloutLabel`, `TimerLabel`, `ComboFrame`, `HypeBar` (a `Frame` inside a `Frame`), `PromptFrame`, `ToastList`, `BooCamFrame`, `MenuFrame`, `ResultsFrame`. - Use the same values and thresholds from `State`: the timer switches to a warning colour at ≤10 seconds; the combo bar width is `comboLeft / 5`; the hype bar is `hype / 100`. - The Boo Cam is just a yellow `Frame` with two eye `Frame`s and a mouth `Frame`; on `ScareResult` set the pupils inward and the mouth to a filled oval for 1.2 seconds. That is the entire joke, and it is six lines of TweenService if you want it to pop. - `Enum.RenderPriority` / `DisplayOrder` should put the prompt above the HUD but below the results screen. Fonts: the web build uses Barlow Condensed and Space Grotesk (OFL). Roblox has `Enum.Font.Bangers` / `Antique` for a close chunky feel without uploading font assets. --- ## 6. Audio `src/client/audio.js` is a WebAudio synth, not audio files. Porting options: 1. **Fastest:** pick Roblox library sounds for boo, scream, clap and score blips; keep the same event triggers (`boo` on E, `scream` + `clap` on a successful scare, `caught` on a tag). 2. **Faithful:** recreate the same envelopes with `Sound` + `SoundGroup`, or use `TweenService` on `PlaybackSpeed`. The original synth is deliberately cheap and short. The clap is timed at ~220 ms after a scare in the web build; keep that if you want the cat and the sound to land together. --- ## 7. Step-by-step port 1. **Copy the rules.** `roblox/src/ReplicatedStorage/BooTag/Config.luau` and `Scoring.luau` are already a direct translation. Do not rewrite the scoring logic by feel; the web build's unit tests in `tests/game.test.mjs` are the spec (first clean boo = 200, second clean = 400, caught = −150, frenzy = ×2, ranks at 900 / 2,200 / 4,200 / 6,800). 2. **Bring the layout.** Recreate `Config.Layout` in `Workspace` (or generate it from the table with a one-off script). Use the same dimensions so the camera and scare range feel right. 3. **Build one NPC.** R6-ish model, anchored, `PivotTo` on the server. Drive state with `BotBrain.step` from a `Heartbeat` accumulator. 4. **Wire remotes.** Add `Remotes.luau` to `ReplicatedStorage`, create the events on the server, `WaitForChild` on the client. 5. **Implement `Scare`.** Range + `isBehind` + cooldown + `Scoring.awardScare` + `BotBrain.spook`, then fire `ScareResult`. 6. **Round loop.** 3-second countdown, 90-second round, `End` payload with `Scoring.rankFor`. 7. **HUD.** Bind `State`; add the prompt using the same client-side nearest-behind test as `refreshPrompt()` in `src/client/main.js`. The server still validates the actual scare. 8. **Player feel.** `WalkSpeed = 16`, sprint = 25, tiptoe = 7, jump = 10.5-style impulse. The default Roblox camera is already the third-person camera the web build imitates. 9. **Polish.** Boo Cam, cat clap, score popups, frenzy overlay, pause/replay. 10. **Test.** Run a two-client session. Confirm a client cannot score from across the map by firing `Scare` manually, and that `scare` during `countdown`/`ended` is ignored. ### Known differences to decide on - **Camera:** Roblox's default camera is not a collision-solving orbit exactly like `player.js`; the feel will be close but not identical. If you need parity, copy the yaw/pitch values (`camDist 16.5`, `camPitch 0.35`) into a custom camera script. - **Input:** Roblox's mobile controls replace `touch.js`; keep the BOO button as a `TextButton` with `Activated` → `Scare`. - **Audio:** the web synth has no asset ids. Pick sounds early so the mix can be tuned. - **Faces:** the web textures are generated canvases. In Studio, use `Decal` images for the five expressions (smile, smug, grin, shocked, angry, cross-eyed) or `EditableImage`. - **Player nameplate:** the web build hides the player's own label and draws a green ring; on Roblox use a `Highlight` with a green outline. --- ## 8. Files in the Rojo project ``` roblox/ ├─ default.project.json ├─ README.md └─ src/ ├─ ReplicatedStorage/BooTag/ │ ├─ Config.luau │ ├─ Scoring.luau │ ├─ BotBrain.luau │ └─ Remotes.luau ├─ ServerScriptService/ │ └─ BooTagServer.server.luau └─ StarterPlayerScripts/ └─ BooTagClient.client.luau ``` These are a **working skeleton**, not a finished place: the server implements the round, NPC state, scare validation and scoring, and the client implements input, HUD, the Boo Cam and the remotes. The block geometry, cat statue, audio ids and NPC animation are the remaining studio work, deliberately left as parts you can build in Studio rather than generated from code. If you only want the rules, `Config.luau` + `Scoring.luau` + `BotBrain.luau` are enough; they have no dependencies on the scene.