Search docs
Search guides, modules and symbols
Modules

Docs / API / Modules / ui

HUD

Game CS2

@s2script/cs2

Custom Panorama HUD: CustomHudLayout.create(spec) plus hudkit (modals, badges, toasts, callouts, banners, MOTD, dashboard). CustomHudSpec.observable (default false) is spectator rendering policy, not confidentiality. Use from OnPluginStart or later; pre-ctx methods throw. Modal/badge null is pool exhaustion (tryModal / tryBadge report PoolExhausted). spec/descriptor stay readable at module top-level. Retained views expose isValid(); modal/dashboard clicks dispatch from the last complete paint. invalidate() coalesces next-frame repaints; refresh() stays synchronous. tryOwn* / tryOwnDashboard claim shared surfaces (Busy instead of overwrite); hideAll preserves explicit claims. Unload/reload clears surfaces; badge.release() hides for every remaining viewer. HudLayout.subscribeClick is a disposable sibling of onClick. ui / hud() / components() are deprecated aliases.

Import

import { HudResult } from "@s2script/cs2";

Author-time types ship in the npm package; the engine injects the runtime at plugin load. Add @s2script/sdk to your plugin's dependencies.

API reference

Generated from the shipped type definitions.

type
HudResult

Result of a drive call: null on success, or a human-readable reason it did not happen. Class/text caches update only after a successful write, so a failed drive does not poison a later retry.

type
UiErrorCode

Stable failure category for structured UI results: NotReady | StaleClient | Released | Unavailable | PoolExhausted | Busy | InvalidArgument | PaintFailed. Branch on error.code; never parse error.message.

type
UiResult

{ ok: true, value } or { ok: false, error: { code, message } }. Failed ops roll back claims/capture. There is no automatic retry queue.

interface
UiSubscription

A route registration that can be removed without affecting other handlers.

dispose(): void
interface
UiSurfaceHandle

An explicit claim on one shared hudkit presentation. Disposal is idempotent; retained views report Released after their owner is disposed.

isValid(): boolean
dispose(): void
— Idempotent. Does not evict another explicit claim.
type
UiStatus

Server-side drive readiness. clientContent is always "unknown" — this API cannot prove that the client's workshop content rendered.

readonly server: "ready" | "not-ready" | "unavailable"
readonly clientContent: "unknown"
— Server-side state cannot prove that the client's workshop content rendered.
readonly reason: string | null
interface
LayoutSlot

One pre-declared row/slot in a pooled collection.

readonly id: string
readonly vars: readonly string[]
interface
CustomHudSpec

What a custom_hud_layout entity will load and how the server may drive it. resource and addons are required. Everything else has a default: hideClass is "s2-hide", text/buttons/meters are empty. HudLayout.set always uses id == dialog variable name; HudLayout.setText uses text when a panel's variable differs.

readonly addons: readonly string[]
— Decimal workshop addon ids clients must mount (via MultiAddonManager).
readonly resource: string
— Source .xml path under panorama/layout/custom_game/.
readonly observable?: boolean
— Show the spectated player's version of this layout to observers. Default false. Requires CS2's September 9, 2026 update. This controls rendering, not network confidentiality or permission to click. Fixed at creation; reusing a resource with a conflicting value in the same plugin throws.
readonly hideClass?: string
— Class that hides a panel when applied. Default "s2-hide".
readonly text?: Readonly<Record<string, string>>
— panelId -> dialog variable name for HudLayout.setText. Default: none.
readonly buttons?: readonly ButtonId[]
— Button ids delivered by HudLayout.onClick / subscribeClick / CustomHudLayoutNs.onClicked. Inline literals constrain subscribeClick ids; a dynamic readonly string[] keeps the string fallback.
readonly meters?: Readonly<Record<string, string>>
— Meter name -> fill panel id (width driven via s2-w0..s2-w10 classes).
readonly slots?: Readonly<Record<string, readonly LayoutSlot[]>>
— Named pools of fixed slots (e.g. list rows). Each slot carries its own var list.
type
LayoutDescriptor
const
PROBE_LAYOUT

Spec for the shipped s2script_hud.xml probe in workshop addon 3790153369. **This targets a PROBE, not a driveable HUD.** The XML carries literal text and has no dialog variable bindings, so a panel driven through it permanently reads "S2SCRIPT PROBE OK". Use CustomHudLayout.probe to answer "is my addon mounted and rendering at all?" — and nothing else. For real UI, hudkit or CustomHudLayout.create with your own layout.

const
DEFAULT_HUD_DESCRIPTOR
interface
CustomHudClickedView

Block-scoped view of one engine CustomHudClicked delivery. Valid only during the handler.

readonly player: EntityRef | null
— The clicking controller, books-gated. Null when unresolved.
readonly slot: number
— 0-based player slot, or -1 when CustomHudClickedView.player did not resolve.
readonly buttonId: string
— Globally scoped button id from the layout markup.
type
OnCustomHudClickedView
interface
HudPlayer

One player's state on a CustomHudLayout. The engine's ForPlayer calls — classes, dialog variables, input capture — are per-slot storage on one entity, not per-recipient delivery. Observation is a rendering policy (CustomHudSpec.observable), not confidentiality. Default false keeps the viewer's own UI while spectating; true shows the spectated player's version. Do not put confidential values here. Unchanged values are not re-sent (the networked intern tables are not free).

readonly slot: number
— 0-based player slot this view drives.
isValid(): boolean
— Whether this retained view still belongs to the current client connection.
show(panelId: string, opts?: { cursor?: boolean }): HudResult
tryShow(panelId: string, opts?: { cursor?: boolean }): UiResult<void>
hide(panelId: string): HudResult
cursor(on: boolean): HudResult
set(id: string, value: string | number): HudResult
— Set text where panel id equals the dialog variable name. Also accepts a field map.
set(fields: Readonly<Record<string, string | number>>): HudResult
setText(panelId: string, value: string): HudResult
— Set text using the spec's text map; falls back to id == variable name.
setText(fields: Readonly<Record<string, string>>): HudResult
setClass(panelId: string, className: string, on: boolean): HudResult
setMeter(meterName: string, percent: number): HudResult
— Set a meter 0..100 (quantized to 10% CSS steps s2-w0..s2-w10).
setPool(poolName: string, entries: readonly (readonly string[])[]): HudResult
setDisabled(buttonId: string, disabled: boolean): HudResult
forget(): void
interface
HudLayout

A plugin-owned custom_hud_layout entity bound to one layout XML. Created with CustomHudLayout.create. The entity is spawned on the first SIGNON_ACTIVE client (never from OnMapStart — that segfaults). Drive it through HudLayout.forSlot; slot-first methods stay for bulk loops.

import { CustomHudLayout } from "@s2script/cs2";
const layout = CustomHudLayout.create({
  addons: ["YOUR_ADDON_ID"],
  resource: "panorama/layout/custom_game/your_hud.xml",
  buttons: ["ok_btn"],
});
layout.onClick("ok_btn", (p) => { p.hide("dialog"); p.cursor(false); });
const p = layout.forSlot(slot);
p.set({ title: "Hello", ok_label: "OK" });
p.show("dialog", { cursor: true });
readonly spec: CustomHudSpec<ButtonId>
readonly layout: CustomHudSpec<ButtonId>
forSlot(slot: number): HudPlayer
— Per-player view of this layout (cached per slot). Storage on this entity, not private delivery. Default observable: false keeps the viewer's own UI while spectating; true shows the spectated player's version.
ensure(): HudResult
— Spawn the layout entity if it is not already in the world. Same timing as CustomHudLayout.create: after a client is SIGNON_ACTIVE. Idempotent.
status(): UiStatus
— Server-side drive readiness. Client workshop/render acknowledgement is not observable.
show(slot: number, panelId: string, opts?: { cursor?: boolean }): HudResult
tryShow(slot: number, panelId: string, opts?: { cursor?: boolean }): UiResult<void>
hide(slot: number, panelId: string): HudResult
cursor(slot: number, on: boolean): HudResult
set(slot: number, id: string, value: string | number): HudResult
set(slot: number, fields: Readonly<Record<string, string | number>>): HudResult
setText(slot: number, panelId: string, value: string): HudResult
setText(slot: number, fields: Readonly<Record<string, string>>): HudResult
setClass(slot: number, panelId: string, className: string, on: boolean): HudResult
setMeter(slot: number, meterName: string, percent: number): HudResult
capacity(poolName: string): number
setPool(slot: number, poolName: string, entries: readonly (readonly string[])[]): HudResult
onClick(buttonId: string, handler: (player: HudPlayer) => void): void
— Route a layout XML button id (from CustomHudSpec.buttons) to a handler. There is no widget object — buttonId is a string the markup already declared. The handler receives the clicking player's HudPlayer. Duplicate handlers throw. May coexist with subscribeClick.
subscribeClick(buttonId: ButtonId, handler: (player: HudPlayer) => void): UiSubscription
— Add a disposable route constrained to literal button ids declared by this layout. May coexist with the legacy single onClick. Delivery snapshots the legacy handler and all subscription callbacks before invoke; subscribe/dispose during a click changes the next delivery only.
setDisabled(slot: number, buttonId: string, disabled: boolean): HudResult
forget(slot: number): void
interface
CustomHudLayoutNs

Load-window custom_hud_layout API. Throws after settle.

create<const ButtonId extends string = string>(spec: CustomHudSpec<ButtonId>): HudLayout<ButtonId>
— Bind (and spawn, once a client is active) a custom_hud_layout for spec. Same layout resource is reused if you call this twice. Inline buttons literals constrain HudLayout.subscribeClick ids.
probe(): HudLayout
— The shipped probe layout. Renders literal "S2SCRIPT PROBE OK" and is not driveable. Use it to confirm the workshop addon is mounted.
onClicked(handler: (view: CustomHudClickedView) => HookResultValue | void): void
— Raw click observer — player + globally scoped button id. Always observes; cannot suppress the engine/map handler. Prefer HudLayout.onClick for typed routing.
readonly kit: HudKit
— Shared panel pool (modals / toasts / badges) over s2script_lib.xml.
toast(slot: number, spec: ToastSpec): HudResult
— Shortcut for HudKit.toast.
readonly PROBE: LayoutDescriptor
— Spec used by CustomHudLayoutNs.probe.
hud<const ButtonId extends string = string>(descriptor: CustomHudSpec<ButtonId>): HudLayout<ButtonId>
hud(descriptor?: CustomHudSpec): HudLayout<string>
components(descriptor?: CustomHudSpec): HudKit
— Component kit for descriptor. Cached by layout resource — components(hudkit.spec) and equivalent copies reuse the existing kit.
createLayout(descriptor?: CustomHudSpec): HudResult
onCustomHudClicked(handler: (view: CustomHudClickedView) => HookResultValue | void): void
type
Hud
type
CtxUi
const
CustomHudLayout

Load-window custom_hud_layout API (import { CustomHudLayout } from "@s2script/cs2"). Throws after settle.

const
ui
type
Variant

Toast / footer-button colouring. ghost is the low-emphasis default for footers.

interface
Row

One list row. Three columns: a is primary and flexes, b and c are right-aligned.

readonly id?: string
— Stable domain identity for this row. Supplied IDs must be unique within the row collection. The library snapshots it with the submitted row; revalidate current permissions, client or entity identity, and item availability by this ID immediately before acting.
readonly a: string
readonly b?: string
readonly c?: string
readonly disabled?: boolean
— Greys the row. Cosmetic only — onPick still fires, so you can say WHY it is unavailable.
readonly tone?: "good" | "warn" | "bad"
— Tints the row's primary cell — green, amber, red. Composes with disabled and with the selection highlight, so an unaffordable-and-dangerous row can say both at once. Requires a workshop addon carrying the .s2-li-* rules. On an older addon the class is set and the client simply has no rule for it, so a tone degrades to an untinted row rather than breaking the sheet — never make a tone the ONLY way a row communicates something.
interface
FooterButton
readonly text: string
readonly variant?: Variant
readonly onClick: (slot: number, view: ModalView) => void
interface
ModalSpec
readonly title: string | ((slot: number) => string)
readonly subtitle?: string | ((slot: number) => string)
— Defaults to a page indicator when the list pages.
readonly rows: readonly Row[] | ((slot: number) => readonly Row[])
— Full list; the library pages it. Called per repaint, so it may read live state.
readonly onPick?: (slot: number, index: number, row: Row, view: ModalView) => void
— Receives the row and absolute index from the last completely submitted page. The source may have changed since then: use row.id to revalidate the current domain object instead of indexing a newly fetched collection with index. Engine submission is not client acknowledgement. Click delivery currently has no render revision, so a delayed click after the same panel is repainted cannot be distinguished.
readonly detail?: (slot: number, row: Row | undefined, cursor: number) => readonly string[]
— Up to 4 detail lines for the selected row. The LAST line renders in a clamped, fixed-height box — put attacker-controlled text there, and escape it before it reaches this call.
readonly buttons?: readonly FooterButton[] | ((slot: number) => readonly FooterButton[])
— Up to 5; Prev/Next claim the trailing two automatically when the list pages. Text, variants, and handlers may differ for each player. Clicks dispatch through the handler table from that player's most recent paint; other players' updates do not change it.
readonly pageSize?: number
readonly width?: "sm" | "md" | "lg" | "xl"
— Sheet width. Default md (560px).
type
UiFocusOptions

Opt in to the shared modal/dashboard/MOTD focus stack; this does not control engine z-order.

readonly mode: "exclusive"
readonly priority?: number
— Higher wins; latest acquisition wins ties. Default 0. Runtime requires an integer in −2147483648…2147483647.
type
ModalOpenResult

A failed open leaves the modal closed and can be retried when the HUD is ready. { ok: true, view } or { ok: false, error }.

interface
ModalView

Per-player view of a claimed Modal.

readonly slot: number
isValid(): boolean
— False after client replacement, component release/forget, or layout replacement. Ordinary close/open cycles keep the same view valid.
open(opts?: { cursor?: boolean; focus?: UiFocusOptions }): ModalView
— Throws a descriptive error when the HUD cannot be painted or shown.
tryOpen(opts?: { cursor?: boolean; focus?: UiFocusOptions }): ModalOpenResult
— Attempt to open; reports paint/show failures without throwing.
tryOpenResult(opts?: { cursor?: boolean; focus?: UiFocusOptions }): UiResult<ModalView>
— Attempt to open with a stable, machine-readable failure category.
close(): void
isOpen(): boolean
— True only after paint and show succeed.
refresh(): void
— Synchronous repaint now. A successful sync repaint fulfills pending invalidation intent.
invalidate(): void
— Schedule one repaint for the next server frame; repeated calls coalesce.
lastUpdateResult(): UiResult<void> | null
— Last completed open or repaint for this live view; pending invalidation leaves it unchanged. Null before the first completed update, and after the view is stale.
tryRefresh(): UiResult<void>
— Repaint this bound player's modal and report the submitted drive result.
page(delta: number): void
select(index: number): void
cursor(): number
forget(): void
interface
BadgeSpec
readonly corner?: "tl" | "tr" | "bl" | "br"
readonly title?: string
readonly accent?: "accent" | "good" | "warn" | "bad"
interface
BadgeView

Per-player view of a claimed Badge.

readonly slot: number
isValid(): boolean
— False after client replacement, badge release/forget, or layout replacement. Ordinary hide/show cycles keep the same view valid.
show(data?: { title?: string; text?: string }): void
tryShow(data?: { title?: string; text?: string }): UiResult<void>
— Repaint this badge and report the submitted drive result.
hide(): void
interface
Badge

A persistent corner element — the thing chat cannot be, because chat scrolls away.

show(slot: number, data?: { title?: string; text?: string }): BadgeView
hide(slot: number): void
forSlot(slot: number): BadgeView
release(): void
interface
ToastSpec
readonly title?: string
readonly message?: string
readonly variant?: Variant
readonly holdSeconds?: number
— 0 keeps it up until something replaces it. Default 6.
interface
CalloutSpec

Hint-style overlay. Bottom-center, no cursor. Default hold 4s.

readonly title?: string
readonly message?: string
readonly variant?: "good" | "warn" | "bad"
readonly holdSeconds?: number
— 0 keeps it up until something replaces it. Default 4.
interface
BannerSpec

Center-top broadcast strip. One at a time, no cursor. Default hold 5s.

readonly text: string
readonly holdSeconds?: number
— 0 keeps it up until something replaces it. Default 5.
interface
MotdSection
readonly heading?: string
readonly body?: string
interface
MotdSpec

First-join / rules overlay. Scrim + OK; not a Menu and not a third center sheet. Stays until OK or MotdHandle.close.

readonly focus?: UiFocusOptions
— Opt in to the shared modal/dashboard/MOTD exclusive-focus stack. A failed focused open logs a diagnostic and returns an invalid no-op MotdHandle.
readonly title: string
readonly subtitle?: string
readonly sections?: readonly MotdSection[]
— Up to 3 heading/body pairs. Unused slots are hidden.
readonly note?: string
readonly ok?: string
— OK button label. Default "OK".
readonly cursor?: boolean
— Default true.
readonly onClose?: (slot: number) => void
— Fired when the player clicks OK. Not fired on programmatic MotdHandle.close.
interface
MotdHandle
readonly slot: number
isValid(): boolean
— False after close/replacement or when its client/component lifetime ends.
close(): void
interface
DashTab

One dashboard tab. id is the grouping key (TopMenu tab / category id).

readonly id: string
— Must be unique within the submitted tab collection.
readonly title: string
interface
DashRow

One option on the active dashboard tab.

readonly id: string
— Must be unique within the active tab's submitted row collection.
readonly a: string
readonly b?: string
readonly disabled?: boolean
interface
DashboardSpec
readonly title: string | ((slot: number) => string)
readonly subtitle?: string | ((slot: number, tabId: string) => string)
readonly closeText?: string
readonly tabs: readonly DashTab[] | ((slot: number) => readonly DashTab[])
— Full tab list; hidden tabs are omitted by the caller.
readonly rows: (slot: number, tabId: string) => readonly DashRow[]
readonly onPick?: (slot: number, tabId: string, row: DashRow, view: DashboardView) => void
— Receives the tab ID and copied row from the last completely submitted page. Revalidate the current domain object, permissions, and client/entity identity by row.id before acting. The engine click payload has no client render revision, so it cannot identify a delayed click from an older repaint of the same panel.
readonly onClose?: (slot: number) => void
— Fired when the player clicks Close. Not fired on programmatic Dashboard.close.
interface
DashboardView
readonly slot: number
isValid(): boolean
— False after client replacement, dashboard spec replacement/forget, or layout replacement. Ordinary close/open cycles keep the same view valid.
open(opts?: { tab?: string; cursor?: boolean; focus?: UiFocusOptions }): DashboardView
tryOpenResult(opts?: { tab?: string; cursor?: boolean; focus?: UiFocusOptions }): UiResult<DashboardView>
— Attempt to open with a stable, machine-readable failure category.
close(): void
isOpen(): boolean
setTab(tabId: string): void
refresh(): void
— Synchronous repaint now. A successful sync repaint fulfills pending invalidation intent.
invalidate(): void
— Schedule one repaint for the next server frame; repeated calls coalesce.
lastUpdateResult(): UiResult<void> | null
— Last completed open or repaint for this live view; pending invalidation leaves it unchanged. Null before the first completed update, and after the view is stale.
tryRefresh(): UiResult<void>
— Repaint this bound player's dashboard and report the submitted drive result.
interface
Dashboard

Tabbed hub over s2_dash on s2script_lib.xml. The legacy controller uses one spec at a time (last configuration wins). Not a modal-pool slot and not a third s2_mN sheet.

open(slot: number, opts?: { tab?: string; cursor?: boolean; focus?: UiFocusOptions }): DashboardView
tryOpenResult(slot: number, opts?: { tab?: string; cursor?: boolean; focus?: UiFocusOptions }): UiResult<DashboardView>
— Attempt to open with a stable, machine-readable failure category.
close(slot: number): void
isOpen(slot: number): boolean
setTab(slot: number, tabId: string): void
refresh(slot?: number): void
— Synchronous. Omit slot to repaint every player who has it open.
invalidate(slot?: number): void
— Schedule open views for one repaint on the next server frame.
tryRefresh(slot: number): UiResult<void>
— Repaint one player and report the submitted drive result.
forSlot(slot: number): DashboardView
interface
OwnedDashboard

An independently disposable dashboard controller with explicit per-player surface claims. Construction is player-independent; tryOpenResult(slot) claims that player's dashboard root.

dispose(): void
— Idempotent. Retained views report Released after their owner is disposed.
interface
HudKitPlayer
readonly slot: number
isValid(): boolean
— Whether this retained player view still belongs to the current client/component lifetime.
toast(spec: ToastSpec): HudResult
— Legacy presentation. Reserves host occupancy while visible.
tryOwnToast(spec: ToastSpec): UiResult<UiSurfaceHandle>
— Explicit toast claim. Reports Busy instead of overwriting a live presentation. Four lanes per player; host-selected allocation.
callout(spec: CalloutSpec): HudResult
— Legacy presentation. Reserves host occupancy while visible.
tryOwnCallout(spec: CalloutSpec): UiResult<UiSurfaceHandle>
— Explicit callout claim. Reports Busy instead of overwriting a live presentation.
banner(spec: BannerSpec): HudResult
— Legacy presentation. Reserves host occupancy while visible.
tryOwnBanner(spec: BannerSpec): UiResult<UiSurfaceHandle>
— Explicit banner claim. Reports Busy instead of overwriting a live presentation.
motd(spec: MotdSpec): MotdHandle
— Legacy presentation. A failed focused open returns an invalid no-op MotdHandle.
tryOwnMotd(spec: MotdSpec): UiResult<UiSurfaceHandle>
— Explicit MOTD claim. Reports Busy instead of overwriting a live presentation. Focused ownership stays linked to the physical focus lease.
hideAll(): void
— Clear this context's pooled panels and legacy/free owned-surface lanes. Preserves every explicit claim, including the caller's.
forget(): void
interface
HudKit

Component handles and operations remain usable in later callbacks after initialization. The ambient hudkit requires plugin context for methods and layout/hud getters; only its static spec/descriptor data is readable at module top-level.

readonly spec: CustomHudSpec
— Static library descriptor. Always readable at module top-level; pass to CustomHudLayout.components(hudkit.spec).
readonly descriptor: CustomHudSpec
— Alias of spec. Static library descriptor — always readable at module top-level.
ensure(): HudResult
— Spawn the pool's layout entity. Same timing as CustomHudLayout.ensure. hudkit also spawns once a client is active.
readonly layout: HudLayout
— The underlying custom_hud_layout, for anything the library does not cover. Throws before plugin context exists — that is not pool exhaustion.
readonly hud: HudLayout
— Alias of layout. Throws before plugin context exists.
modal(spec: ModalSpec): Modal | null
— Claim a pooled modal. Null when all are in use.
tryModal(spec: ModalSpec): UiResult<Modal>
— Claim a pooled modal and expose pool exhaustion as a structured result.
dashboard(spec: DashboardSpec): Dashboard
— Bind the legacy shared TopMenu dashboard controller. Last spec wins. Always returns a handle (the panel is a single root, not a pool). Each open reserves host occupancy for that player. Throws before plugin context exists.
tryOwnDashboard(spec: DashboardSpec): UiResult<OwnedDashboard>
— Construct an independent dashboard controller; each player is claimed when opened. An explicit claim never evicts another explicit claim; Busy if the root is occupied.
badge(spec?: BadgeSpec): Badge | null
— Claim a pooled corner badge. Null when all are in use.
tryBadge(spec?: BadgeSpec): UiResult<Badge>
— Claim a pooled corner badge and expose pool exhaustion as a structured result.
toast(slot: number, spec: ToastSpec): HudResult
callout(slot: number, spec: CalloutSpec): HudResult
banner(slot: number, spec: BannerSpec): HudResult
motd(slot: number, spec: MotdSpec): MotdHandle
forSlot(slot: number): HudKitPlayer
hideAll(slot: number): void
— Clear this context's pooled panels and legacy shared surfaces; explicit claims are preserved. Modal/badge pools close only this plugin context's live claims.
forget(slot: number): void
budget(): {
    panelIds: number;
    classNames: number;
    variables: number;
    declared: number;
    warnAt: number;
    cap: number;
  }
— Names interned so far, per engine vector. Each has its OWN cap of 1024 (not one shared 3072), and all are shared by every plugin on the HUD entity — past the cap a name is refused and its value simply never arrives. set spends from two vectors at once, charging its panel id and its dialog variable name to different ledgers. Interning is idempotent, so these climb only on a name's FIRST use; repainting is free.
type
Components
const
hudkit

Shared panel pool over s2script_lib.xml. Use from OnPluginStart or later callbacks. Methods and layout/hud getters throw before the plugin context exists (module top-level). Static spec/descriptor data is always readable. Modal/badge claims return null only when their pool is exhausted. Same object as CustomHudLayoutNs.kit.

import { hudkit } from "@s2script/cs2";
import { command, HookResult } from "@s2script/sdk";

export function OnPluginStart(): void {
  const menu = hudkit.modal({
    title: "Players",
    rows: [{ a: "Alice" }, { a: "Bob" }],
    buttons: [{ text: "Close", onClick: (_s, v) => v.close() }],
  });
  command("sm_players", cmd => {
    const slot = cmd.callerSlot;
    if (slot < 0) return HookResult.Handled;
    menu?.forSlot(slot).open();
    hudkit.forSlot(slot).toast({ title: "Saved", message: "loadout written" });
    hudkit.callout(slot, { message: "Press E for the next corner" });
    hudkit.banner(slot, { text: "Vote passed — changing map" });
    hudkit.motd(slot, { title: "Server rules", sections: [{ heading: "Don't cheat", body: "VAC is on." }] });
    return HookResult.Handled;
  });
}
Back to modules All packages