Communitygithub.com

Wend4r/s2r-skills

Uma coleção de habilidades de agente para trabalhar com recursos Source 2 e desenvolver código relacionado.

O que é s2r-skills?

Esta skill, parte da coleção Wend4r/s2r-skills, fornece um guia detalhado e prático para desenvolvedores que desejam criar HUDs personalizados no Source 2 usando a entidade `custom_hud_layout`. Ela explica de forma abrangente as limitações e o fluxo de trabalho correto para criar interfaces dentro das restrições do motor, que só permite quatro tipos de painel (Panel, Label, Image, Button) e nenhum script no lado do cliente. A skill detalha a pipeline completa, desde a definição do layout no servidor até a renderização no cliente, e diferencia os dois modos de funcionamento: 'Overlay', para elementos visuais passivos sem interação, e 'Cursor', que ativa um cursor e permite que cliques em botões sejam enviados ao servidor. Além disso, oferece uma tabela de correspondência entre conceitos da web e Panorama, um exemplo prático de um indicador de ponto de captura com markup XML, estilos VCSS e código JavaScript do servidor, e uma seção de depuração com mensagens de erro comuns. É uma referência essencial para qualquer desenvolvedor criando HUDs para mods ou plugins de servidor no Source 2.

Funciona com~Claude Code~Codex CLI✓Cursor
npx skills add Wend4r/s2r-skills

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Source 2 | Layout de HUD Personalizado (custom_hud_layout)

A entidade custom_hud_layout (C++ CCSCustomHudLayout) permite que um mapa ou plugin do servidor apresente aos jogadores um HUD criado em Panorama XML + VCSS.

Leia isto primeiro: um HUD personalizado não é um documento Panorama comum. O XML carregado passa por um validador de lista de permissões dedicado que permite apenas quatro tipos de painel e um conjunto fixo de atributos. Não há style=, nem <script>, e nenhum manipulador de eventos na marcação. Todo o comportamento dinâmico é controlado a partir do servidor.

A pipeline completa

custom_hud_layout (servidor, keyvalue layout="...")
   │  m_strLayout é replicado para o cliente
   ▼
CSGOCustomHuds (janela Panorama) → layout/hud/customhuds.xml
   └─ <Panel class="WindowRoot" hittest="false">
        └─ <CSGOCustomHud id="CustomHud" style="width:100%; height:100%; flow-children:none;">
             └─ CSGOCustomHudLayoutRoot   ← criado a partir de C++, um por entidade,
                  └─ SEU XML                sem id e sem class

Caminho de retorno (um clique): mensagem do usuário CS_UM_CustomHudClicked (390) → evento JS do servidor OnCustomHudClicked carregando { player, layout, buttonId }.

Dois tipos de HUD

Cada HUD personalizado é uma de duas coisas, e a diferença é uma única chamada por jogador:

Overlay — o padrãoCursor — SetInputCaptureEnabled(slot, true)
O que épixels na telauma tela que o jogador pode usar
Mousenenhum; a mira continua apontandoum cursor aparece sobre o HUD
Cliquesnada é clicávelcliques em Button alcançam o servidor
:hovernunca correspondefunciona e não custa nada ao servidor
Construçãotemporizadores, placares, feed de mortes, bannerslojas, diálogos de voto, seletores, menus

A captura está desligada toda vez que o layout é construído, portanto, precisa ser ativada novamente após a entidade surgir — veja entity.md, Input capture. Dentro de um HUD capturado, hittest="false" decide por quais painéis o cursor passa; escreva-o em tudo e remova-o dos poucos contêineres que devem capturar o mouse.

O que um HUD pode realmente conter

O vocabulário de marcação são quatro tags, mas o que você pode colocar por meio delas é mais amplo do que parece:

  • Ícones SVG (.vsvg) que permanecem nítidos em qualquer escala de interface, tingidos em tempo de execução com wash-color;
  • suas próprias fontes, colocando arquivos .ttf na pasta panorama/fonts/ do addon;
  • texto localizado por jogador, via text="#Token" resolvido no lado do cliente no idioma de cada jogador, usando suas próprias strings ou todo o catálogo do jogo base;
  • a arte do jogo base, referenciada pelo caminho s2r:// sem adicionar nada ao seu download;
  • animações e transições, incluindo pares de entrada/saída acionados por alternâncias de classe;
  • vídeo, como uma camada background-image em um Panel comum.

Se você está vindo da web

Panorama tem a forma de HTML/CSS, o que é uma ajuda e uma armadilha: a sintaxe parece familiar, e então uma propriedade silenciosamente não faz nada. O modelo mental que funciona é uma página renderizada no servidor sem nenhum script do cliente, onde as únicas duas atualizações que existem são alternar uma classe em um painel e definir uma string em um painel. Todo o resto — cada variante, cada estado, cada ramo — é criado antecipadamente e meramente revelado.

Você escreveriaAqui você escreve
<div><Panel>
<span>, um nó de texto<Label text="..."> — o texto vive em um atributo, não entre tags
<img src><Image src>, ou um background-image em qualquer painel
<button onclick><Button id> — o clique vai para o servidor, não para um script
style="..."nada; class é o único gancho de estilização
display: nonevisibility: collapse
visibility: hiddenopacity: 0 — ainda no layout, ainda clicável
position: absolutex / y mais ignore-parent-flow: true
flexbox, gridflow-children no pai, horizontal-align / vertical-align no filho
gapmargin nos filhos
filter: blur(4px)blur: gaussian( 4, 4, 1 )
rgba(0, 0, 0, 0.5)#00000080 — cores são apenas hexadecimais, alfa é o último byte
linear-gradient(...) em background-imagegradient( ... ) em background-**color**
@font-facecoloque o .ttf em panorama/fonts/; nada a declarar
:hover:hover — uma das poucas coisas que funciona exatamente como esperado
um manipulador de evento JSo evento do servidor OnCustomHudClicked, indexado pelo id do botão
data-* lido por JSvariáveis de diálogo {s:nome}, definidas a partir do servidor
um loop ou <template>nenhum existe — enumere as linhas na marcação ou na folha de estilo
calc(), propriedades personalizadasnenhuma existe — enumere os valores na folha de estilo

Dois hábitos do trabalho na web causam a maior parte do tempo perdido aqui. Recorrer a um valor calculado: não há calc(), nem variáveis, e o servidor não pode enviar um número, então uma barra de progresso é uma classe por etapa e uma coordenada é um literal em uma regra. E recorrer a comportamento do cliente: um menu suspenso que abre sozinho, uma guia que alterna sozinha, um formulário que se valida sozinho — nada disso pode acontecer sem uma ida e volta ao servidor, então projete a interação em torno de um clique indo para o servidor e uma classe voltando.

O que sobrevive intacto é a parte que os designers mais se importam: o modelo de caixa, alinhamento, cor, tipografia, sombras, cantos arredondados, transições e animações keyframe se comportam próximos o suficiente da web para que o trabalho visual seja transferido diretamente. Veja references/css.md para o conjunto completo de propriedades e a lista completa do que está ausente.

Início rápido

Um exemplo prático: um indicador de ponto de captura — uma faixa somente leitura ancorada na parte inferior da tela com um anel de progresso radial, controlado inteiramente por variáveis de diálogo e alternância de classes. Não precisa de captura de entrada, porque nada nele é clicável.

Os arquivos de origem ficam em panorama/layout/custom_game/ e panorama/styles/custom_game/ na raiz de conteúdo do addon; o compilador os transforma em .vxml_c / .vcss_c:

panorama/layout/custom_game/capture_point.xml
panorama/styles/custom_game/capture_point.css

1. Marcação — panorama/layout/custom_game/capture_point.xml

<root>
	<styles>
		<include src="s2r://panorama/styles/custom_game/capture_point.vcss_c" />
	</styles>

	<!--
		O painel externo NÃO deve ter um id — o carregador o atribui e o compilador
		rejeita um. Mantenha-o como um wrapper anônimo de tela cheia e coloque a raiz real dentro.
	-->
	<Panel class="cp-screen" hittest="false">

		<!-- Começa oculto: a entidade mostra este layout para TODOS os jogadores no momento em que surge, então o estado criado tem que ser o vazio. -->
		<Panel id="CaptureRoot" class="cp-root hidden" hittest="false">

			<Panel class="cp-ring">
				<Panel class="cp-ring-track" hittest="false" />
				<!-- O servidor reescreve a classe de cp-fill para cp-fill-0 … cp-fill-100;
				     a folha de estilo transforma cada etapa em um sweep clip: radial(). -->
				<Panel id="CaptureFill" class="cp-ring-fill cp-fill-0" hittest="false" />
				<Label id="CapturePct" class="cp-pct" text="{s:pct}" />
			</Panel>

			<Panel class="cp-copy">
				<Label id="CaptureName" class="cp-name" text="{s:point_name}" />
				<Label id="CaptureHint" class="cp-hint" text="{s:hint}" />
			</Panel>

		</Panel>
	</Panel>
</root>

2. Estilos — panorama/styles/custom_game/capture_point.css (não há estilos inline)

/* Wrapper de tela cheia. noclip permite que o brilho do anel se espalhe para além da borda do painel em vez de
   ser cortado; o wrapper em si nunca é testado para hit. */
.cp-screen
{
	width: 100%;
	height: 100%;
	overflow: noclip;
}

/* A faixa em si, estacionada acima da borda inferior. Começa invisível e deslocada, e o
   servidor adiciona "show" para reproduzir a revelação — é por isso que esta é uma alternativa para .hidden,
   não uma adição: um painel recolhido está fora do layout e não tem nada para animar. */
.cp-root
{
	flow-children: right;
	horizontal-align: center;
	vertical-align: bottom;
	margin-bottom: 96px;
	opacity: 0;
	transform: translateY(20px);
	transition: opacity 0.15s ease-out 0.0s, transform 0.15s ease-out 0.0s;
}

/* O estado revelado. Ambas as propriedades estão listadas na transição acima, então adicionar e
   remover a classe anima em cada direção. */
.cp-root.show
{
	opacity: 1;
	transform: translateY(0px);
}

/* Remove um painel do layout completamente, para que os vizinhos se aproximem em vez de deixar um buraco.
   Nada mais recolhe um painel — opacity: 0 mantém o espaço reservado. */
.hidden
{
	visibility: collapse;
}

/* Caixa quadrada onde as duas imagens do anel são empilhadas. Ambas a preenchem, então se alinham exatamente. */
.cp-ring
{
	width: 56px;
	height: 56px;
}

/* O anel não preenchido abaixo, escurecido para que o preenchimento no topo seja lido como progresso. A arte vem
   de uma textura compilada via s2r:// — a forma padrão. */
.cp-ring-track
{
	width: 100%;
	height: 100%;
	background-image: url( "s2r://panorama/images/custom_game/ring_png.vtex" );
	background-size: contain;
	background-repeat: no-repeat;
	opacity: 0.25;
}

/* A mesma arte no topo, recolorida por wash-color e revelada um setor de cada vez pelas
   regras .cp-fill-* abaixo. Tingir uma fonte cinza é melhor do que enviar uma textura por equipe. */
.cp-ring-fill
{
	width: 100%;
	height: 100%;
	background-image: url( "s2r://panorama/images/custom_game/ring_png.vtex" );
	background-size: contain;
	background-repeat: no-repeat;
	wash-color: #4caf6dff;
}

/* Uma regra por etapa que o servidor pode selecionar, porque ele pode alternar uma classe, mas não pode enviar um
   número. clip não tem custo de layout e anima, então mais etapas são baratas. */
.cp-fill-0
{
	clip: radial( 50% 50%, 0deg, 0deg );
}

.cp-fill-25
{
	clip: radial( 50% 50%, 0deg, 90deg );
}

.cp-fill-50
{
	clip: radial( 50% 50%, 0deg, 180deg );
}

.cp-fill-75
{
	clip: radial( 50% 50%, 0deg, 270deg );
}

.cp-fill-100
{
	clip: radial( 50% 50%, 0deg, 360deg );
}

/* Coluna de texto à direita do anel, centralizada em relação a ele. */
.cp-copy
{
	flow-children: down;
	vertical-align: center;
	margin-left: 12px;
}

/* Nome do ponto — a linha que o jogador lê primeiro. */
.cp-name
{
	font-size: 20px;
	font-weight: bold;
	color: #ffffffff;
}

/* Linha de suporte, suavizada com alfa em vez de uma segunda cor. */
.cp-hint
{
	font-size: 14px;
	color: #ffffff99;
}

/* Destaque da equipe. O servidor define team-ct ou team-t na raiz, e o seletor descendente
   repinta o preenchimento — então a cor da equipe e o progresso permanecem independentes um do outro. */
.cp-root.team-ct .cp-ring-fill
{
	wash-color: #56a0ddff;
}

.cp-root.team-t .cp-ring-fill
{
	wash-color: #f0a531ff;
}

3. Entidade — no Hammer ou via CreateEntityByName

KeyvalueValor
layoutpanorama/layout/custom_game/capture_point.vxml

4. Controlando a partir do servidor (JS do lado do servidor em cs_script) — veja entity.md

const hud = Instance.FindEntityByName("capture_hud"); // Classe wrapper JS: CustomHudLayout

function showCapture(slot, name, pct, team) {
	hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", true);
	hud.SetHasClassForPlayer(slot, "CaptureRoot", "team-" + team, true);

	// troca a classe da etapa: limpa a antiga, define a nova
	hud.SetHasClassForPlayer(slot, "CaptureFill", "cp-fill-" + roundTo25(pct), true);

	hud.SetDialogVariableStringForPlayer(slot, "CapturePct", "pct", pct + "%");
	hud.SetDialogVariableStringForPlayer(slot, "CaptureName", "point_name", name);
	hud.SetDialogVariableStringForPlayer(slot, "CaptureHint", "hint", "Segure o ponto");
}

function hideCapture(slot) {
	hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", false);
}

Como o servidor só pode alternar classes e definir strings — não pode enviar um número — um valor contínuo como um anel de progresso precisa ser quantizado em uma classe por etapa. Cinco etapas são suficientes para um anel de captura; uma barra mais suave precisa apenas de mais regras.

Rejeições comuns

O que você querPor que falha
<TextEntry>, <ProgressBar>, <Movie>, <ToggleButton>, qualquer outra tagapenas Panel, Label, Image, Button — para vídeo use uma camada background-image em vez de <Movie>
style="..." em um elementonenhum tipo de painel tem um atributo style
onactivate="..." ou qualquer manipulador de evento na marcaçãonenhum; cliques alcançam o servidor indexados por id
<scripts>, <snippets>, <snippet>rejeitado no nível do tipo de nó AST
<include> de outro .xmlapenas um recurso .vcss pode ser referenciado
dialogvariable="..." como um atributoo servidor define variáveis via SetDialogVariableString
hittest em <Button>Button aceita apenas id e class

Conjunto completo permitido: xml.md.

Depuração

O validador registra no canal de log custom_hud no nível de aviso:

MensagemCausa
Layout xml is an invalid resource name "%s"layout não resolve para um nome de recurso
Failed to load layout '%s'.arquivo ausente ou não compilado
Layout contains disallowed node '%s' (type: %d).<scripts>/<snippets>/<snippet> etc.
Layout contains disallowed panel type '%s'.tag fora das quatro permitidas
Layout contains disallowed attribute %s for panel type '%s'.atributo não na lista dessa tag
Layout contains reference to disallowed resource type '%s'.<include> tem como alvo um recurso que não é .vcss
Layout xml did not pass CustomHud validation "%s"rejeição final
Layout is invalid.um nó de atributo cujo pai não é um painel

Em uma falha de validação, o painel nunca é criado — uma tela em branco, sem renderização parcial. Recarregar um layout ativo em um estado inválido destrói o HUD existente.

Referências

ArquivoConteúdo
xml.mdLista de permissões completa de tags/atributos, tipos de nós AST, regras do validador
css.mdTodas as 140 propriedades CSS do Panorama, unidades, conjuntos de valores de fluxo/alinhamento, equivalências de web para Panorama
entity.mdAtributos de entidade, captura de entrada, variáveis de diálogo, alternância de classes, rede, total de bytes, multiquadro
js.mdClasse CustomHudLayout para scripts de servidor, chamadas de método, configuração de retorno de chamada, slots, eventos
overlay_multipanel.mdVárias instâncias de HUD em uma única entidade, casos de uso, limites
gradients.mdFunção gradient(), parâmetros, paradas de cor, dicas de design, exemplos de VCSS
implementation.mdCCSCustomHudLayout, CSGOCustomHudLayoutRoot, layout, opacidade temporária, canal custom_hud

Habilidades Relacionadas