From dea38ff684fad7f1fc528e2c45d39608773117ce Mon Sep 17 00:00:00 2001 From: KHeartz Date: Tue, 13 Oct 2026 23:24:56 -0400 Subject: [PATCH] docs: add camera, audio and voice chat guides New Camera and sound section (orders 70-79), written against 1.8.2: the client's custom camera, server and client sounds, and proximity voice chat with its ranges, tiers and mutes. The overview's camera example now orbits the player and restores on a second key press instead of restoring in the same tick. The sample checker also lets JavaScript pass an untyped event argument on, as it already lets it read one. --- README.md | 2 +- guides/Camera and sound/audio.md | 90 +++++++++++++++ guides/Camera and sound/camera.md | 152 ++++++++++++++++++++++++++ guides/Camera and sound/voice-chat.md | 138 +++++++++++++++++++++++ guides/overview.md | 24 +++- scripts/check_samples.mjs | 3 +- 6 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 guides/Camera and sound/audio.md create mode 100644 guides/Camera and sound/camera.md create mode 100644 guides/Camera and sound/voice-chat.md diff --git a/README.md b/README.md index 2a41fc2..fc4c814 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The closed-source Mod remains authoritative for generated client and server API ## Structure -- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 10–19, Core concepts 20–29, Players 30–39, NPCs and creatures 40–49, World 50–59, User interface 60–69, Hosting a server 90–99. +- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 10–19, Core concepts 20–29, Players 30–39, NPCs and creatures 40–49, World 50–59, User interface 60–69, Camera and sound 70–79, Hosting a server 90–99. - Image directories live beside the Markdown document that references them. - `docs.config.json` owns the generator pin, branding, links, navigation, and community-content mapping. - `scripts/sync_contract.mjs` downloads and verifies the public scripting contract. diff --git a/guides/Camera and sound/audio.md b/guides/Camera and sound/audio.md new file mode 100644 index 0000000..d0fbe63 --- /dev/null +++ b/guides/Camera and sound/audio.md @@ -0,0 +1,90 @@ +--- +title: Audio +description: Play the game's sounds at a place, on a player or for one player, stop sounds that keep playing, load soundbanks and change the music, from the server or a client. +sidebar: + order: 71 +--- + +Every sound in the game is a Wwise event, named like `cabbage_explosion` or `accio_cast`. The server's `Audio` global plays events on players' clients, and a client's `Audio` plays them for its own player. Event names ignore case, and positions and ranges are world centimetres. + +## Playing sounds from the server + +| Call | Who hears it | +| --- | --- | +| `Audio.playSoundAt(event, x, y, z, options)` | Players within `range` of the point, from that point | +| `Audio.playSoundOnPlayer(player, event, options)` | Players within `range` of the player, from the player as they move, the player included | +| `Audio.playSoundForPlayer(player, event, options)` | Only that player, at full volume wherever they are | +| `Audio.playSoundForAll(event, options)` | Every connected player, at full volume wherever they are | + +`range` is 10000 (100 m) by default, and 0 sends the sound to everyone. Each call returns a sound id for `Audio.stopSound(id)`: + +```js +// server/main.js +function thunderAt(x, y, z) { + return Audio.playSoundAt("Amb3D_Storm_ThunderClaps_Play", x, y, z, { range: 20000 }); +} +``` + +The server sends a sound once, to the players in range as it starts. A player who arrives or joins afterwards does not hear it, and a player still on a loading screen misses it. + +Sounds ignore [virtual worlds](/guides/core-concepts/virtual-worlds/#what-ignores-worlds): a player within range hears a sound whatever world they are in. To keep a sound to one world, send it to that world's players and [play it on their clients](#playing-sounds-on-the-client). + +## Sounds that keep playing + +Some events play once and end, and others, such as loops and charging spells, keep playing until something stops them. `Audio.stopEventFor(event)` returns the name of the event that stops one, or an empty string for a sound that ends by itself. + +Pass `duration` in seconds to end a sound by itself, or keep its id and call `Audio.stopSound(id)`. Stopping a sound plays its stop event first, looked up from the game's soundbanks, and `stopEvent` names a different one: + +```js +// server/main.js +function castEffect(player) { + Audio.playSoundOnPlayer(player, "accio_cast", { duration: 3 }); +} +``` + +On the server, `Audio.stopAllSounds()` stops every sound on every client, the game's own music and ambience included, for when a sound keeps playing and nobody has its id. A client's `Audio.stopAll()` does the same for that player. + +## Playing sounds on the client + +A client's `Audio.playSoundAt(event, x, y, z, options)` plays a sound only its own player hears, from a point. `Audio.playSound(event, options)` plays one on the player's own character, at full volume. Both take `duration` and `stopEvent`, and return an id for `Audio.stopSound`, or 0 when the game refused the sound. `Audio.stopOnSelf()` stops everything playing on the player's character. + +This pair plays a sound for the players in one world only: + +```js +// server/main.js +function playInWorld(world, event, x, y, z) { + for (const player of PlayerManager.getAll()) { + if (player.virtualWorld === world) player.emit("sound:play", JSON.stringify({ event, x, y, z })); + } +} +``` + +```js +// client/main.js +Events.on("sound:play", (sound) => { + Audio.playSoundAt(sound.event, sound.x, sound.y, sound.z); +}); +``` + +## Soundbanks + +Wwise keeps events in soundbanks, and the game loads its banks as players move around. When a sound's bank is not loaded, the client loads it and tries the sound again, so you rarely need to load a bank yourself. `Audio.bankFor(event)` names the bank an event needs. + +`Audio.loadBank(name)` and `Audio.unloadBank(name)` load and unload a bank, on every client from the server or on one client from a client script. A bank of your own is a `.bnk` file that must be in the game's `Phoenix/Content/WwiseAudio/Windows/` folder on each player's machine. + +## Music + +`Audio.setMusic(event, priority)` puts a track on the game's music stack, at `priority` 5 by default, which plays over the game's own score. `Audio.clearMusic(event)` takes it off, and whatever was underneath plays again. Only events that start a track work: most of the game's music events only switch the score between moods. A name you have cleared cannot be used again in the same session. + +`Audio.setState(group, state)` and `Audio.setRTPC(name, value)` set Wwise states and parameters for the whole sound engine, mostly useful with soundbanks of your own. + +Music, states, parameters and loaded banks are sent from the server once, to the players connected at the time, so set them again for players who join later. + +## The player's volume + +`Settings.getAudio()` reads the player's own volume settings, from 0 to 1, and `Settings.setAudio(patch, options)` changes them. These are the player's choices, so only write them from a settings panel they opened. Voice chat has its own volume, [`Voice.setVolume`](/guides/camera-and-sound/voice-chat/#on-the-client). + +## Where to go next + +- [Voice chat](/guides/camera-and-sound/voice-chat/) for players talking to each other. +- [Camera](/guides/camera-and-sound/camera/) for the pictures to go with the sound. diff --git a/guides/Camera and sound/camera.md b/guides/Camera and sound/camera.md new file mode 100644 index 0000000..7d7e25e --- /dev/null +++ b/guides/Camera and sound/camera.md @@ -0,0 +1,152 @@ +--- +title: Camera +description: Take over the player's view with a camera of your own, move it in shots, paths and orbits, fade the screen, and hand the view back. +sidebar: + order: 70 +--- + +Each client has one custom camera that client scripts drive with the `Camera` global. While it is active, the player sees through it instead of the game's own camera, and `Camera.restore()` hands the view back. Positions are world centimetres, rotations are degrees and durations are seconds. + +The camera only changes what the player sees. Their character still answers their input, so stop it with `LocalPlayer.stopPlayerInput()` for a cutscene, and hand it back with `LocalPlayer.restorePlayerInput()`. + +## Taking over the view + +`Camera.capture()` returns the game camera's current view, as a position, a rotation and a lens, or `null` in a menu or behind a loading screen. `Camera.activate(view, transition)` switches to a fixed camera at a view, blending over half a second by default: + +```js +// client/main.js +function showVault() { + Camera.clearLookAt(); + Camera.activate( + { position: { x: 15200, y: -3400, z: 900 }, rotation: { pitch: -20, yaw: 90, roll: 0 } }, + { duration: 1.5, curve: "easeInOut" }, + ); +} + +function endShot() { + if (Camera.active) Camera.restore({ duration: 1 }); +} +``` + +A transition takes a `duration`, and a `curve` of `"linear"`, `"cubic"`, `"easeIn"`, `"easeOut"`, `"easeInOut"` (the default) or `"preBlended"`. `Camera.restore(transition)` blends back to the player's own camera the same way. + +### One camera for every resource + +The custom camera belongs to the client, not to the resource that activated it. Every resource drives the same one, and stopping a resource does not give the view back, so restore it yourself as your resource stops: + +```js +// client/main.js +Events.on("resourceStop", (resourceName) => { + if (resourceName === "intro-cutscene" && Camera.active) Camera.restore({ duration: 0 }); +}); +``` + +Check `Camera.active` before restoring. `restore` always points the view back at the player's character, so calling it while your camera is not active takes the view from whatever else has it, such as the character creator. + +Hand the view back before the player respawns or travels to another map, and activate again afterwards if you still need the camera. + +## Moving the camera + +Moves, paths, orbits and `lookAt` need an active camera and return `false` without one: + +| Call | Does | +| --- | --- | +| `Camera.setPose({ position, rotation })` | Moves the camera at once | +| `Camera.animateTo(view, { duration, easing })` | Moves it to a view over `duration` seconds, 1 by default | +| `Camera.playPath(keyframes, { loop, pingPong })` | Flies it through up to 256 keyframes | +| `Camera.orbit(target, { radius, height, speed })` | Circles it around a point | +| `Camera.lookAt(target, track)` | Aims it at a point, and with `track` keeps aiming while it moves | + +`easing` is `"linear"`, `"smoothStep"`, `"easeIn"`, `"easeOut"` or `"easeInOut"`, the default. `Camera.pause()`, `Camera.resume()` and `Camera.stop()` control whatever is moving, and `stop` leaves the camera where it is. + +Nothing fires when a move ends. `Camera.playing` stays true while a move, path or orbit runs, so check it on a timer. + +### Paths + +A path starts at its first keyframe at once. Every later keyframe sets how long the camera takes to reach it, with `duration` (1 second by default) and `easing`. A keyframe without a `lens` keeps the previous one's. This intro flies over the grounds with the player held still, then hands back control: + +```js +// client/main.js +const start = { position: { x: 0, y: 0, z: 800 }, rotation: { pitch: -10, yaw: 0, roll: 0 } }; + +function playIntro() { + LocalPlayer.stopPlayerInput(); + Camera.clearLookAt(); + Camera.activate(start, { duration: 0 }); + Camera.playPath([ + start, + { position: { x: 3000, y: 0, z: 900 }, rotation: { pitch: -15, yaw: 30, roll: 0 }, duration: 6 }, + { position: { x: 3000, y: 2500, z: 1200 }, rotation: { pitch: -25, yaw: 90, roll: 0 }, duration: 5, easing: "easeOut" }, + ]); + + const timer = setInterval(() => { + if (Camera.playing) return; + clearInterval(timer); + Camera.restore({ duration: 1 }); + LocalPlayer.restorePlayerInput(); + }, 250); +} +``` + +`loop` repeats the path until you stop it, and `pingPong` plays it back and forth, for a menu backdrop that keeps moving. + +### Orbits + +`Camera.orbit(target, options)` circles a point at `radius` centimetres (300 by default), `height` centimetres above it (0 by default) and `speed` degrees per second (20 by default; negative goes the other way). It keeps the camera aimed at the point. + +An orbit starts from wherever the camera is around the point and jumps straight onto the circle, so activate the camera on the circle first: + +```js +// client/main.js +function circlePlayer() { + const pos = LocalPlayer.getPosition(); + const view = Camera.capture(); + if (!pos || !view) return; + + Camera.clearLookAt(); + Camera.activate({ position: { x: pos.x + 400, y: pos.y, z: pos.z + 150 }, rotation: view.rotation }, { duration: 1 }); + Camera.orbit(pos, { radius: 400, height: 150, speed: 15 }); +} +``` + +### Clearing the aim + +An orbit, or `lookAt`, leaves the camera aimed at its point after the orbit stops, after `stop` and even after `restore`. The aim then overrides the rotation of every later shot until `Camera.clearLookAt()` lifts it, so clear it before you set up a new shot, as the samples above do. + +## Lens + +`Camera.setLens(options)` changes the active camera's lens: `fov` is the horizontal field of view in degrees, between 0 and 180, and `focalLength`, `aperture` and `postProcessBlend` are there for cinematic shots. A view passed to `activate` or `animateTo`, or a keyframe, can carry a `lens` too. `Camera.lens` reads the lens back. + +```js +// client/main.js +Camera.setLens({ fov: 30 }); +``` + +## Fades + +`Camera.fade(options)` fades the screen to a colour, whether or not your camera is active. It goes from opacity `from` (0 by default) to `to` (1 by default) over `duration` seconds (1 by default), in black unless you give a `color` with components from 0 to 1. + +When a fade finishes, the screen clears again unless you pass `hold: true`. Fade out with `hold`, do what you need behind the curtain, then fade back in: + +```js +// client/main.js +function cutTo(view) { + Camera.fade({ to: 1, duration: 0.5, hold: true }); + setTimeout(() => { + Camera.clearLookAt(); + Camera.activate(view, { duration: 0 }); + Camera.fade({ from: 1, to: 0, duration: 0.5 }); + }, 500); +} +``` + +`Camera.stopFade()` clears a fade at once. `restore` does not, so stop a held fade yourself. + +## Shakes and other effects + +`Camera.playShake(classPath, { scale, space })` plays one of the game's camera shakes by its full class path, and `Camera.stopShakes()` stops them. `Camera.playAnimation`, `Camera.addLensEffect` and `Camera.addModifier` do the same for the game's camera animations, lens effects and modifiers. A modifier cannot be removed again. + +## Where to go next + +- [HUD, prompts and nametags](/guides/user-interface/hud-and-nametags/#hiding-the-hud) for hiding the HUD during a cutscene. +- [Audio](/guides/camera-and-sound/audio/) for the sound that goes with it. diff --git a/guides/Camera and sound/voice-chat.md b/guides/Camera and sound/voice-chat.md new file mode 100644 index 0000000..32fea71 --- /dev/null +++ b/guides/Camera and sound/voice-chat.md @@ -0,0 +1,138 @@ +--- +title: Voice chat +description: Who hears whom in proximity voice chat, how far voices carry, whispering and shouting, muting players, and the voice settings each player controls. +sidebar: + order: 72 +--- + +Players talk to the people near them, holding push-to-talk, which is V by default. The server's `Voice` global decides how far voices carry and who may talk or listen, and each client's `Voice` controls that player's own microphone and volume. Ranges are world centimetres. + +## Who hears whom + +A player hears someone talking when: + +- the talker is within their voice's range, measured between the two characters; +- the two can see each other's [virtual worlds](/guides/core-concepts/virtual-worlds/), so separate worlds are separate conversations; +- the talker is not muted, the listener is not deafened, neither has turned voice chat off, and the listener has not muted the talker. + +A voice is at full volume close by and fades to silence at the edge of its range. A talker also has to be near enough for the listener's game to have them loaded, so a range beyond that distance adds nothing. Each player hears at most six people at once. + +## How far voices carry + +Each player talks on a tier: whisper, normal or shout. HogwartsMP starts them at 8 m, 25 m and 60 m: + +| Tier | Number | Range | +| --- | --- | --- | +| Whisper | 0 | 800 | +| Normal | 1 | 2500 | +| Shout | 2 | 6000 | + +| Call | Does | +| --- | --- | +| `Voice.setRange(range)` | Sets the normal range, which every player without a range of their own uses | +| `Voice.setTierRange(tier, range)` | Sets one tier's range; 0 makes it carry the normal range | +| `Voice.setPlayerRange(player, range)` | Gives one player a range of their own, whatever their tier; 0 takes it away | +| `Voice.setPlayerTier(player, tier)` | Moves a player to a tier | + +`getRange()`, `getTierRange(tier)`, `getPlayerRange(player)` and `getPlayerTier(player)` read them back. + +`Voice.setRange` with 0 or less does not restore HogwartsMP's 25 m: it sets 25 cm, which silences everyone. Pass `2500` to go back. + +```js +// server/main.js +// Whoever holds the stage is heard across the hall. +function giveStage(player) { + Voice.setPlayerRange(player, 6000); +} + +function takeStage(player) { + Voice.setPlayerRange(player, 0); +} +``` + +### Whispering and shouting + +HogwartsMP gives players no key for changing tier, so a resource that wants them to whisper or shout binds one on the client. `Voice.setTier(tier)` asks the server to move the player, and the server raises `playerVoiceTierChange` with the player and their new tier: + +```js +// client/main.js +const TIER_NAMES = ["Whispering", "Talking", "Shouting"]; + +Key.bind("f3", () => { + const tier = (Voice.getTier() + 1) % 3; + Voice.setTier(tier); + Game.notify(`${TIER_NAMES[tier]}.`); +}); +``` + +`Voice.setPlayerTier` from the server does not raise the event. + +## Muting players + +| Call | Does | +| --- | --- | +| `Voice.setPlayerMuted(player, muted)` | Nobody hears the player | +| `Voice.setPlayerDeaf(player, deaf)` | The player hears nobody | +| `Voice.setLocalMute(listener, target, muted)` | One player stops hearing another | + +`isPlayerMuted`, `isPlayerDeaf` and `isLocallyMuted` read them back. Changes take effect within a quarter of a second. The muted player is not told, and their voice events keep firing, so tell them yourself. + +```js +// server/main.js +// The dead tell no tales. +Events.on("playerDied", (player) => Voice.setPlayerMuted(player, true)); +Events.on("playerDownedEnd", (player) => Voice.setPlayerMuted(player, false)); +``` + +All of these belong to the player's connection: they last until the player disconnects, and are gone when they come back. They also outlast the resource that set them, so undo them when your resource stops if it should not leave players muted. + +## Who is talking + +`playerVoiceStart` and `playerVoiceStop` fire on the server with the player as they start and stop talking, and `Voice.isPlayerTalking(player)` reads it. Silence is not sent, so a pause of more than about 0.3 seconds stops and starts again, and no `playerVoiceStop` fires when a player disconnects mid-sentence. + +Clients cannot see who else is talking, so the server tells them. This marks talkers on their nametags: + +```js +// server/main.js +Events.on("playerVoiceStart", (player) => Events.emitAllClients("voice:talking", { playerId: player.id, talking: true })); +Events.on("playerVoiceStop", (player) => Events.emitAllClients("voice:talking", { playerId: player.id, talking: false })); +``` + +```js +// client/main.js +Events.on("voice:talking", (update) => { + if (update.talking) Nametags.setLabel(update.playerId, "(speaking)", 0); + else Nametags.clearLabel(update.playerId); +}); +``` + +`Voice.isPlayerVoiceEnabled(player)` tells you whether a player left voice chat on in their settings. It is their preference, not a permission: mute or deafen to enforce anything. + +## On the client + +A client's `Voice` controls the player's own voice chat: + +| Call | Does | +| --- | --- | +| `Voice.setEnabled(enabled)` | Turns voice chat on or off for this player | +| `Voice.setVolume(volume)` | Sets how loud others are, where 1 is normal and 4 the most | +| `Voice.setHearingRange(range)` | Narrows how far this player hears; it cannot widen the server's ranges | +| `Voice.setPushToTalkKey(key)` | Changes push-to-talk, using the [key names](/guides/user-interface/keys-and-menus/#binding-keys) `Key.bind` takes | +| `Voice.setPushToTalkReleaseDelay(ms)` | Keeps sending for up to 2000 ms after the key is released, 100 by default | +| `Voice.setTransmitMode(mode)` | `"pushToTalk"`, or `"voiceActivity"` to send whenever the microphone is loud enough | +| `Voice.setActivationThreshold(level)` | How loud, from 0 to 1, voice activation needs; `Voice.getInputLevel()` reads the microphone for a level meter | +| `Voice.setNoiseSuppression(enabled)` | Filters steady background noise | +| `Voice.setInputDevice(name)` | Picks a microphone from `Voice.getInputDevices()`, or the default for an empty name | + +Each has a getter, and `Voice.hasMicrophone()` tells you whether the player has one working. `voiceStart` and `voiceStop` fire on the client as the player starts and stops talking, and `Voice.isTalking()` reads it. + +Push-to-talk sends nothing while the chat is open, a [web view](/guides/user-interface/web-views/#focus-and-input) has focus, the controls are locked or the game is in the background. Voice activation keeps sending through all of them. + +Nothing here is saved: the settings last until the game closes, and follow the player to other servers they join meanwhile. A resource with a voice settings panel saves the player's choices itself and applies them again when they join. + +Voice chat plays outside the game's sound engine, so the game's volume settings and the `Audio` calls do not change it. + +## Where to go next + +- [Virtual worlds](/guides/core-concepts/virtual-worlds/) for separating groups of players, their voices included. +- [Audio](/guides/camera-and-sound/audio/) for the game's own sounds. diff --git a/guides/overview.md b/guides/overview.md index e7cbf93..c7015c4 100644 --- a/guides/overview.md +++ b/guides/overview.md @@ -52,6 +52,12 @@ Use the navigation to browse the globals available to the selected environment. - [Map markers](/guides/user-interface/map-markers/) — markers, search circles and routes on the map, player blips, and the minimap. - [Keys and menus](/guides/user-interface/keys-and-menus/) — key bindings, locking the controls, and keeping the game's own menus shut. +**Camera and sound:** + +- [Camera](/guides/camera-and-sound/camera/) — taking over the view with shots, paths and orbits, fades, and handing it back. +- [Audio](/guides/camera-and-sound/audio/) — the game's sounds from the server or a client, soundbanks and music. +- [Voice chat](/guides/camera-and-sound/voice-chat/) — who hears whom, voice ranges and tiers, muting, and each player's voice settings. + **Running a server:** - [Hosting a server](/guides/hosting-a-server/run-a-server/) — installing on Windows or Linux, Foundations, ports, the masterlist, and updates. @@ -109,13 +115,19 @@ if (pos) { console.log(pos.x, pos.y, pos.z); } -const view = Camera.capture(); -if (view) { - Camera.activate(view); - Camera.orbit(view.position, { radius: 300, height: 100, speed: 25 }); -} +Key.bind("f6", () => { + if (Camera.active) { + Camera.clearLookAt(); + Camera.restore({ duration: 0.5, curve: "easeOut" }); + return; + } -Camera.restore({ duration: 0.5, curve: "easeOut" }); + const target = LocalPlayer.getPosition(); + const view = Camera.capture(); + if (!target || !view) return; + Camera.activate({ position: { x: target.x + 300, y: target.y, z: target.z + 100 }, rotation: view.rotation }); + Camera.orbit(target, { radius: 300, height: 100, speed: 25 }); +}); ``` > Server and client declarations must be loaded separately. A global shown in one environment is not automatically available in the other. diff --git a/scripts/check_samples.mjs b/scripts/check_samples.mjs index 12ed0aa..fd25f11 100644 --- a/scripts/check_samples.mjs +++ b/scripts/check_samples.mjs @@ -25,12 +25,13 @@ const options = { // Samples elide the helpers, variables and sibling files around them, so a missing lowercase name or a relative // import is the reader's own code. A missing capitalized name looks like a runtime global and is reported. In // JavaScript, custom event payloads and exports are simply values, so reading one, even once a truthiness check -// narrows it to `{}`, is not reported either; a TypeScript sample has to narrow them as its reader would. +// narrows it to `{}`, or passing one on is not reported either; a TypeScript sample has to narrow them as its reader would. const ignored = (diagnostic, extension) => { const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); const name = /'([^']+)'/.exec(message)?.[1] ?? ""; if (diagnostic.code === 2307) return /^\.\.?\//.test(name); if (extension === ".js" && (diagnostic.code === 18046 || diagnostic.code === 2571)) return true; + if (extension === ".js" && diagnostic.code === 2345) return message.startsWith("Argument of type 'unknown'"); if (extension === ".js" && diagnostic.code === 2339) return message.endsWith("on type '{}'."); if (![2304, 2552, 2584, 18004].includes(diagnostic.code)) return false; return /^[a-z_$]/.test(name);