diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 000000000..9e1bce73a --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,242 @@ +# autofix — o bot conserta o MECÂNICO e deixa o PR pronto. Ele NÃO mergeia. +# +# POR QUE EXISTE (medido em 21/08/2026, nos últimos 29 PRs): nenhum bot deste +# repositório jamais commitou um conserto. O #405 ficou vermelho por +# `DOCS1: documentação DESATUALIZADA`, cujo conserto é `npm run docs` mais um +# commit — trabalho sem julgamento nenhum, que hoje ou espera um mantenedor ou +# devolve o PR para o autor. +# +# A TRAVA: `scripts/ci/autofix_allowlist.py`. O bot só commita se TUDO que ele +# mexeu for arquivo gerado. Encostou em qualquer outro caminho, aborta e comenta. +# Sem essa lista, o primeiro conserto errado reescreve o mapa de um colaborador e +# ninguém repara — e a confiança de todo mundo de fora vai junto. +# +# `pull_request_target` porque é o único gatilho que enxerga secret em PR de fork +# (ver a régua WSEC1). O checkout é do MERGE do PR e roda apenas ferramentas do +# repositório base sobre ele — nunca `npm run` de script vindo do fork. +name: autofix + +on: + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review] + # A MAIN ANDANDO é o que desatualiza PR, não o PR se mexendo. Foram 9 `chore(release)` + # em 20 horas, e cada um reescreve README/STATUS/ARCH/docs — reabrindo conflito em TODO + # PR aberto ao mesmo tempo. Sem este gatilho o autofix só acorda quando o autor empurra, + # ou seja, nunca para quem está parado esperando revisão, que é justamente quem precisa. + push: + branches: [main] + workflow_dispatch: + inputs: + pr_number: + description: PR a consertar + required: true + type: number + +permissions: + contents: read + pull-requests: write + +concurrency: + group: autofix-${{ github.event.pull_request.number || inputs.pr_number || github.sha }} + cancel-in-progress: true + +jobs: + # Varredura pós-release: descobre quem ficou para trás e dispara o autofix de cada um + # pelo `workflow_dispatch`, que é o caminho que enxerga secret. Barato — quem já está + # em dia sai no primeiro passo do autofix, sem clonar nada pesado. + varredura: + if: github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + GH_TOKEN: ${{ secrets.CSBRASIL_BOT_TOKEN }} + REPO: ${{ github.repository }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: Dispara o autofix de quem ficou para trás + run: | + if [ -z "$GH_TOKEN" ]; then + echo "::warning::CSBRASIL_BOT_TOKEN ausente — varredura não roda, e não reprova ninguém." + exit 0 + fi + gh pr list --repo "$REPO" --state open --base main \ + --json number,mergeable,isDraft \ + --jq '.[] | select(.isDraft | not) | select(.mergeable != "MERGEABLE") | .number' > /tmp/atrasados.txt + echo "PRs desatualizados: $(wc -l < /tmp/atrasados.txt)" + while read -r pr; do + [ -n "$pr" ] || continue + echo " → #$pr" + gh workflow run autofix.yml --repo "$REPO" -f pr_number="$pr" \ + || echo "::warning::não deu para disparar o autofix do #$pr" + done < /tmp/atrasados.txt + + autofix: + if: github.event_name != 'push' && github.event.pull_request.draft != true + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + GH_TOKEN: ${{ secrets.CSBRASIL_BOT_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} + REPO: ${{ github.repository }} + steps: + - name: Guard de identidade + run: | + if [ -z "$GH_TOKEN" ]; then + echo "::warning::CSBRASIL_BOT_TOKEN ausente — autofix não roda, e não reprova ninguém por isso." + echo "pular=1" >> "$GITHUB_ENV" + fi + + - name: Dados do PR + if: env.pular != '1' + run: | + gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json headRefName,headRepository,headRepositoryOwner,maintainerCanModify > /tmp/pr.json + { + echo "HEAD_REF=$(jq -r .headRefName /tmp/pr.json)" + echo "HEAD_REPO=$(jq -r '.headRepositoryOwner.login + "/" + .headRepository.name' /tmp/pr.json)" + # `maintainerCanModify` é FALSE por definição quando o PR não vem de fork — + # a opção nem existe ali. Sem esta conta o bot nunca empurrava em PR de casa, + # que é justamente onde ele sempre pode (medido no #425). + echo "PODE_EDITAR=$(jq -r 'if (.headRepositoryOwner.login + "/" + .headRepository.name) == env.REPO then "true" else (.maintainerCanModify|tostring) end' /tmp/pr.json)" + } >> "$GITHUB_ENV" + + # Checa o código do PR para poder REGERAR a doc a partir dele. Nada do fork é + # executado: os geradores abaixo são os do repositório base, restaurados logo + # depois do checkout. + - name: Checkout do PR + if: env.pular != '1' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + repository: ${{ env.HEAD_REPO }} + ref: ${{ env.HEAD_REF }} + token: ${{ secrets.CSBRASIL_BOT_TOKEN }} + fetch-depth: 0 + + - name: Traz a base e resolve conflito de arquivo gerado + if: env.pular != '1' + id: merge + run: | + git config user.name "csbrasil-bot" + git config user.email "csbrasil-bot@users.noreply.github.com" + git remote add base "https://github.com/$REPO.git" 2>/dev/null || true + BASE="${{ github.event.pull_request.base.ref || 'main' }}" + git fetch base "$BASE" + # A mensagem vai EXPLÍCITA mesmo quando o merge não conflita: `--no-edit` usa a + # mensagem automática do git, que não leva `Agent:` nem `Signed-off-by` — e aí o + # bot conserta o PR e o dco reprova o commit que ele mesmo fez (medido no #406). + if git merge --no-edit -m "Merge da base (autofix)" \ + -m "Agent: csbrasil-bot (autofix)" \ + -m "Signed-off-by: csbrasil-bot " FETCH_HEAD; then + git diff --quiet HEAD@{1} HEAD || echo "mesclou=1" >> "$GITHUB_OUTPUT" + echo "PR já está em cima da base (ou mesclou limpo)." + exit 0 + fi + git diff --name-only --diff-filter=U > /tmp/conflitos.txt + echo "conflitos:"; cat /tmp/conflitos.txt + # A trava é lida da BASE, não do PR: um PR que reescrevesse o allowlist + # liberaria a si mesmo. Extrair para /tmp mantém a árvore limpa para o merge. + git show FETCH_HEAD:scripts/ci/autofix_allowlist.py > /tmp/allowlist-base.py + if ! python3 /tmp/allowlist-base.py --caminhos < /tmp/conflitos.txt; then + git merge --abort + echo "humano=1" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Só gerado: fica o da BASE, e os geradores re-derivam logo abaixo. + xargs -a /tmp/conflitos.txt -r git checkout --theirs -- + xargs -a /tmp/conflitos.txt -r git add -- + git commit --no-edit -m "Merge da base (conflito só em arquivo gerado, resolvido pelo autofix)" \ + -m "Agent: csbrasil-bot (autofix)" \ + -m "Signed-off-by: csbrasil-bot " + echo "mesclou=1" >> "$GITHUB_OUTPUT" + + # As ferramentas que vão rodar têm de ser as da BASE, nunca as do fork — senão + # um PR malicioso reescreve o gerador e o bot executa o que ele mandar. + - name: Restaura as ferramentas da base + if: env.pular != '1' + run: | + git fetch base "${{ github.event.pull_request.base.ref || 'main' }}" + # O lock vem JUNTO com o package.json: `npm ci` exige que os dois batam, e + # restaurar só um deles quebra a instalação (medido em 22/08, 2 de 10 runs). + git checkout FETCH_HEAD -- tools/ scripts/ package.json package-lock.json + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + if: env.pular != '1' + with: { node-version: 22 } + - name: Instala dependências + if: env.pular != '1' + # `npm ci` morre se lock e manifesto divergirem; aqui a instalação é meio para + # rodar o gerador, não o alvo da medição, então cair para `install` é aceitável. + run: npm ci --ignore-scripts || npm install --ignore-scripts --no-audit --no-fund + + # RESOLVE CONFLITO DE ARQUIVO GERADO. Todo `chore(release)` na main reescreve + # README, STATUS, ARCH e docs/, e reabre conflito em TODO PR aberto ao mesmo + # tempo — medido em 21/08: o alpha.173 sozinho pôs quatro PRs em CONFLICTING, e + # os nove arquivos do #400 eram gerados, nenhum de código. Resolver isso à mão é + # trabalho de Sísifo: o release seguinte desfaz. + # + # A trava é a MESMA do resto do autofix. Sobrou UM conflito fora da lista, o bot + # aborta o merge e devolve para gente — conflito de código é julgamento, e + # julgamento não é dele. + - name: Comenta quando o conflito é de gente + if: env.pular != '1' && steps.merge.outputs.humano == '1' + run: | + gh pr comment "$PR_NUMBER" --repo "$REPO" --body \ + "🤖 **autofix**: a base andou e o conflito encostou em arquivo de código — não é meu para resolver. Os arquivos em conflito estão no log do run; rode \`git merge origin/${{ github.event.pull_request.base.ref || 'main' }}\` e decida." + + - name: Conserta o mecânico + if: env.pular != '1' && steps.merge.outputs.humano != '1' + run: | + node tools/gen-docs.mjs || echo "::warning::gen-docs falhou" + node tools/gen-arch.mjs || echo "::warning::gen-arch falhou" + + # As ferramentas foram restauradas da base só para rodar; o que volta ao PR é + # apenas o resultado delas. + - name: Devolve as ferramentas ao estado do PR + if: env.pular != '1' && steps.merge.outputs.humano != '1' + run: git checkout HEAD -- tools/ scripts/ package.json package-lock.json + + - name: A trava — só arquivo gerado pode virar commit + if: env.pular != '1' && steps.merge.outputs.humano != '1' + id: trava + run: | + git status --porcelain > /tmp/mexidos.txt + if [ ! -s /tmp/mexidos.txt ]; then + echo "nada=1" >> "$GITHUB_OUTPUT" + echo "PR já está em dia — nada a consertar." + exit 0 + fi + if ! python3 scripts/ci/autofix_allowlist.py < /tmp/mexidos.txt > /tmp/permitidos.txt; then + cat /tmp/permitidos.txt + echo "bloqueado=1" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "commitar=1" >> "$GITHUB_OUTPUT" + + - name: Comenta quando a trava bloqueia + if: env.pular != '1' && steps.trava.outputs.bloqueado == '1' + run: | + gh pr comment "$PR_NUMBER" --repo "$REPO" --body \ + "🤖 **autofix**: o conserto encostou em arquivo fora da lista de permissão, então **não commitei nada**. Rode \`npm run docs && node tools/gen-arch.mjs\` e confira o que mudou." + + - name: Empurra o conserto + if: env.pular != '1' && (steps.trava.outputs.commitar == '1' || steps.merge.outputs.mesclou == '1') && env.PODE_EDITAR == 'true' + run: | + git config user.name "csbrasil-bot" + git config user.email "csbrasil-bot@users.noreply.github.com" + if [ -s /tmp/permitidos.txt ]; then + xargs -a /tmp/permitidos.txt -r git add -- + fi + git diff --cached --quiet || git commit -m "chore(docs): regenera bloco derivado (autofix)" \ + -m "Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão." \ + -m "Signed-off-by: csbrasil-bot " + git push origin "HEAD:$HEAD_REF" + gh pr comment "$PR_NUMBER" --repo "$REPO" --body \ + "🤖 **autofix**: regenerei os blocos derivados e empurrei o commit. O PR não precisa de mais nada disso." + + # Fork com "allow edits by maintainers" desligado: o bot não tem como empurrar. + # Comentar o comando é melhor que um vermelho que o autor não entende. + - name: Sem permissão de push — comenta o comando + if: env.pular != '1' && (steps.trava.outputs.commitar == '1' || steps.merge.outputs.mesclou == '1') && env.PODE_EDITAR != 'true' + run: | + gh pr comment "$PR_NUMBER" --repo "$REPO" --body \ + "🤖 **autofix**: dá para consertar sozinho, mas este PR está com *allow edits by maintainers* desligado. Rode \`npm run docs && node tools/gen-arch.mjs\` e commite — ou ligue a opção e eu faço." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index daeff137d..e8a62c4d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,6 +122,8 @@ jobs: # Régua se prova antes de medir o PR: portão quebrado passa calado. - name: Autoteste das réguas de CI run: | + python3 scripts/ci/ensure_labels.py --selftest + python3 scripts/ci/autofix_allowlist.py --selftest python3 scripts/ci/dco_check.py --selftest python3 scripts/ci/agente_check.py --selftest - name: Check Signed-off-by diff --git a/.github/workflows/csbrasil-bot-automerge.yml b/.github/workflows/csbrasil-bot-automerge.yml index bff31b52c..b4e9afe0f 100644 --- a/.github/workflows/csbrasil-bot-automerge.yml +++ b/.github/workflows/csbrasil-bot-automerge.yml @@ -45,6 +45,8 @@ jobs: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: python-version: '3.13' + - name: Garante a etiqueta de pronto + run: python3 scripts/ci/ensure_labels.py pronto-pra-merge - name: Fetch PR status run: gh pr view "$PR_NUMBER" --json isDraft,labels,reviewDecision,mergeStateStatus,statusCheckRollup,files > /tmp/automerge.json - name: Check eligibility @@ -53,13 +55,24 @@ jobs: run: | python3 scripts/ci/check_automerge.py --selftest python3 scripts/ci/check_automerge.py < /tmp/automerge.json > /tmp/automerge-result.json - - name: Merge when eligible + # DEIXAR PRONTO NÃO É MERGEAR (decisão do dono, 21/08). Este passo chamava + # `gh pr merge --squash --auto` e fechava o PR sozinho. A intenção declarada dos + # bots é outra: autocorreção e autorrevisão para deixar o PR PRONTO, com o botão + # de merge continuando humano. O que era merge vira etiqueta e um comentário que + # diz por que o PR está pronto. + - name: Marca como pronto quando elegível run: | python3 - <<'PY' import json, os, subprocess, sys data = json.load(open('/tmp/automerge-result.json')) + pr = os.environ['PR_NUMBER'] if not data.get('eligible'): - print('automerge: not eligible') + print('pronto-pra-merge: not eligible') + subprocess.run(['gh', 'pr', 'edit', pr, '--remove-label', 'pronto-pra-merge'], check=False) sys.exit(0) - subprocess.run(['gh', 'pr', 'merge', os.environ['PR_NUMBER'], '--squash', '--delete-branch', '--auto'], check=True) + subprocess.run(['gh', 'pr', 'edit', pr, '--add-label', 'pronto-pra-merge'], check=True) + subprocess.run(['gh', 'pr', 'comment', pr, '--body', + '🤖 **csbrasil-bot**: portões verdes e diff dentro do que o ' + '`check_automerge` aceita — marquei como `pronto-pra-merge`. ' + 'O merge continua sendo seu.'], check=True) PY diff --git a/.github/workflows/csbrasil-bot-pr-classify.yml b/.github/workflows/csbrasil-bot-pr-classify.yml index 1cbc3ab18..6410dde65 100644 --- a/.github/workflows/csbrasil-bot-pr-classify.yml +++ b/.github/workflows/csbrasil-bot-pr-classify.yml @@ -11,6 +11,13 @@ on: types: [submitted, dismissed] pull_request_review_comment: types: [created] + # Varredura: em PR vindo de FORK, os dois gatilhos de review acima rodam SEM os + # secrets do repositório (regra do GitHub), e o job abaixo não pode nem se + # identificar. O sweep roda no contexto base, com token, e reclassifica os PRs + # abertos pelo `workflow_dispatch` — é o que devolve a cobertura pós-CodeRabbit + # que os gatilhos de review davam nos PRs de casa. + schedule: + - cron: '7,37 * * * *' workflow_dispatch: inputs: pr_number: @@ -31,7 +38,17 @@ concurrency: cancel-in-progress: true jobs: + # PORTÃO QUE NÃO PODE MEDIR NÃO VOTA (mesma regra do #402 para o portão de autoria em + # clone raso). `pull_request_review` e `pull_request_review_comment` num PR de FORK não + # recebem secret nenhum: o job não tem como se identificar, falhava no guard e pintava + # de vermelho um PR cujo autor não tem o que consertar. Medido em 11 dos 14 PRs de fork + # dos últimos 29 - e em 0 dos 15 de casa. O run de `pull_request_target` do MESMO commit + # passa e é ele que vale; para o resto, o sweep de meia em meia hora cobre. classify: + if: >- + github.event_name == 'pull_request_target' || + github.event_name == 'workflow_dispatch' || + github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 10 env: @@ -153,3 +170,27 @@ jobs: subprocess.run(['gh', 'api', '-X', 'POST', f"repos/${{ github.repository }}/issues/{os.environ['PR_NUMBER']}/comments", '--input', '-'], input=json.dumps({'body': body}), text=True, check=True) PY + + # Reclassifica os PRs abertos pelo caminho que TEM secret. Idempotente: o classify + # dedupe rótulo e comentário, então rodar de novo não polui o PR. + sweep: + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + GH_TOKEN: ${{ secrets.CSBRASIL_BOT_TOKEN }} + REPO: ${{ github.repository }} + steps: + - name: Guard de identidade + run: | + if [ -z "$GH_TOKEN" ]; then + echo "::error::CSBRASIL_BOT_TOKEN ausente — o sweep precisa do PAT para disparar o classify." + exit 1 + fi + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: Dispara o classify de cada PR aberto + run: | + for pr in $(gh pr list --repo "$REPO" --state open --limit 50 --json number --jq '.[].number'); do + gh workflow run csbrasil-bot-pr-classify.yml --repo "$REPO" -f pr_number="$pr" \ + || echo "::warning::não deu para reclassificar o PR #$pr" + done diff --git a/.github/workflows/portao-browser.yml b/.github/workflows/portao-browser.yml index 9b2c9ab45..ba63fb994 100644 --- a/.github/workflows/portao-browser.yml +++ b/.github/workflows/portao-browser.yml @@ -129,6 +129,11 @@ jobs: if: ${{ !cancelled() }} run: npm run eval:entrada + # A partida carrega as armas DELA e nenhum bot empunha caixa (medido no navegador). + - name: eval:armas (preload de arma por partida) + if: ${{ !cancelled() }} + run: npm run eval:armas + # Número diz que caiu; imagem diz por quê. Só quando algum passo acima reprova. - name: Fotos das piores células (só em falha) if: failure() diff --git a/.github/workflows/preview-bot.yml b/.github/workflows/preview-bot.yml index 4d8fae77f..bf1bb97da 100644 --- a/.github/workflows/preview-bot.yml +++ b/.github/workflows/preview-bot.yml @@ -26,6 +26,9 @@ jobs: REPO: ${{ github.repository }} run: gh pr edit "$PR" --repo "$REPO" --remove-label "preview-autorizado" 2>/dev/null || true + # O job `preview` abaixo só dispara em `labeled` com esta etiqueta. Ela nunca foi + # criada no repositório, então o caminho inteiro era código morto: o bot pedia um + # rótulo que ninguém tinha como aplicar. Criar aqui é o que liga o mecanismo. - name: classifica o diff sem executar código do fork env: GH_TOKEN: ${{ github.token }} @@ -43,50 +46,9 @@ jobs: fi gh pr comment "$PR" --repo "$REPO" --body "🤖 **cs-brasil-ai-bot**: $TEXTO \`preview-autorizado\`." - preview: - if: >- - github.event.pull_request.head.repo.full_name != github.repository && - github.event.action == 'labeled' && - github.event.label.name == 'preview-autorizado' - runs-on: ubuntu-latest - timeout-minutes: 20 - environment: preview-forks - steps: - - name: confirma mantenedor, label e SHA aprovado - env: - GH_TOKEN: ${{ github.token }} - ACTOR: ${{ github.actor }} - REPO: ${{ github.repository }} - PR: ${{ github.event.pull_request.number }} - EVENT_SHA: ${{ github.event.pull_request.head.sha }} - run: | - PERMISSION=$(gh api "repos/$REPO/collaborators/$ACTOR/permission" --jq .permission 2>/dev/null || echo none) - case "$PERMISSION" in admin|maintain|write) ;; *) echo "ator sem permissão de mantenedor"; exit 1 ;; esac - API_SHA=$(gh api "repos/$REPO/pulls/$PR" --jq .head.sha) - [ "$API_SHA" = "$EVENT_SHA" ] - gh api "repos/$REPO/issues/$PR/labels" --jq 'any(.[]; .name == "preview-autorizado")' | grep -qx true - - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - ref: ${{ github.event.pull_request.head.sha }} - persist-credentials: false - allow-unsafe-pr-checkout: true - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: { node-version: 22 } - - run: npm i -g vercel@58.9.0 - - name: publica preview - id: deploy - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - run: | - vercel pull --yes --environment=preview --token "$VERCEL_TOKEN" - vercel build --token "$VERCEL_TOKEN" - URL=$(vercel deploy --prebuilt --token "$VERCEL_TOKEN") - echo "url=$URL" >> "$GITHUB_OUTPUT" - - name: comenta URL - env: - GH_TOKEN: ${{ github.token }} - run: | - gh pr comment "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" --body "🤖 **cs-brasil-ai-bot**: preview no ar → ${{ steps.deploy.outputs.url }}" +# O JOB `preview` SAIU DAQUI (22/08). Ele publicava com `vercel build`, ou seja, +# executava o build do fork com o VERCEL_TOKEN no ambiente — e era por isso que +# exigia um mantenedor aprovar cada push. O preview agora é feito em duas metades +# que não precisam de aprovação nenhuma: `preview-build.yml` compila o código do +# fork SEM segredo, e `preview-deploy.yml` publica COM segredo sem executar nada +# do PR. Este arquivo ficou só com a classificação do diff, que continua útil. diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml new file mode 100644 index 000000000..4bc310732 --- /dev/null +++ b/.github/workflows/preview-build.yml @@ -0,0 +1,81 @@ +# preview-build — COMPILA o PR sem ter nada para roubar. +# +# Metade 1 de 2 do preview de fork. A regra que faz isso funcionar sem aprovação +# humana: este job roda o código do FORK e por isso NÃO recebe segredo nenhum. +# `pull_request` (e não `pull_request_target`) garante isso — num PR de fork o +# GitHub roda com token só-leitura e sem acesso a `secrets`. +# +# O que sai daqui é BYTE ESTÁTICO: o `npm run build` do Astro com adapter da Vercel +# já produz `.vercel/output` inteiro, que é exatamente o que `vercel deploy +# --prebuilt` consome. Quem publica é o `preview-deploy.yml`, que tem o segredo e +# NÃO executa nada do fork. +# +# Antes disso o preview de fork dependia de um mantenedor aprovar cada push, porque +# o desenho antigo rodava `vercel build` (código do fork) com o VERCEL_TOKEN no +# ambiente. Separar as duas metades resolve os dois lados de uma vez: preview +# automático e token que nunca encosta em código de terceiro. +name: preview-build + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +# Só leitura. Este job não comenta, não rotula e não publica. +permissions: + contents: read + +concurrency: + group: preview-build-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + build: + if: github.event.pull_request.draft != true + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: { node-version: 22, cache: npm } + + - run: npm ci + + # Os pacotes de asset vêm de release público — sem segredo, e com retry para + # soluço de rede não virar PR vermelho (é a DG2). + - name: Assets + run: | + bash scripts/fetch-audio.sh + bash scripts/fetch-decals.sh + npm run strip:decalbg + npm run assert:assets + + - name: Build + run: npm run build + + # O número do PR viaja junto: o job de deploy roda em `workflow_run`, onde o + # contexto não sabe de qual PR veio. + - name: Anota o PR + run: | + mkdir -p .vercel/output + echo "${{ github.event.pull_request.number }}" > /tmp/pr-numero.txt + echo "${{ github.event.pull_request.head.sha }}" > /tmp/pr-sha.txt + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: preview-${{ github.event.pull_request.number }} + path: | + .vercel/output + retention-days: 3 + if-no-files-found: error + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: preview-meta-${{ github.event.pull_request.number }} + path: | + /tmp/pr-numero.txt + /tmp/pr-sha.txt + retention-days: 3 + if-no-files-found: error diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml new file mode 100644 index 000000000..659f53bce --- /dev/null +++ b/.github/workflows/preview-deploy.yml @@ -0,0 +1,96 @@ +# preview-deploy — PUBLICA o que já veio compilado, sem executar nada do fork. +# +# Metade 2 de 2 do preview de fork. Este job TEM o `VERCEL_TOKEN`, e por isso a +# regra dele é uma só: nunca rodar código que veio do PR. Ele não faz checkout da +# branch do fork, não roda `npm ci`, não roda script de build. Baixa o artefato +# produzido pelo `preview-build.yml` — bytes estáticos de `.vercel/output` — e +# chama `vercel deploy --prebuilt`, que apenas envia arquivo. +# +# `workflow_run` é o que torna isso possível: ele roda no contexto do repositório +# BASE, com segredo, mesmo quando o PR original veio de fork. +# +# ATENÇÃO A QUEM FOR MEXER: qualquer passo aqui que execute conteúdo do artefato +# (um `npm run`, um `node` sobre arquivo baixado, um `bash` de script do fork) +# desfaz a separação inteira e devolve o token para as mãos de quem abriu o PR. É +# o que a régua PRV3 guarda. +name: preview-deploy + +on: + workflow_run: + workflows: [preview-build] + types: [completed] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: preview-deploy-${{ github.event.workflow_run.id }} + cancel-in-progress: false + +jobs: + deploy: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Baixa o artefato compilado + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + pattern: preview-* + path: /tmp/artefato + + - name: Lê de qual PR veio + id: pr + run: | + NUM=$(cat /tmp/artefato/preview-meta-*/pr-numero.txt 2>/dev/null | tr -dc '0-9') + SHA=$(cat /tmp/artefato/preview-meta-*/pr-sha.txt 2>/dev/null | tr -dc '0-9a-f') + # Confere contra a API em vez de confiar no arquivo: ele veio de um job que + # rodou código do fork, então é entrada não confiável. + case "$NUM" in ''|*[!0-9]*) echo "número de PR inválido no artefato"; exit 1 ;; esac + REAL=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$NUM" --jq .head.sha) + if [ "$REAL" != "$SHA" ]; then + echo "::warning::o PR #$NUM avançou desde o build ($SHA != $REAL) — preview descartado" + echo "pular=1" >> "$GITHUB_OUTPUT" + fi + echo "numero=$NUM" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ github.token }} + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + if: steps.pr.outputs.pular != '1' + with: { node-version: 22 } + - run: npm i -g vercel@58.9.0 + if: steps.pr.outputs.pular != '1' + + # `--prebuilt` só empacota e envia o diretório. Nenhum script do PR roda aqui. + - name: Publica + id: deploy + if: steps.pr.outputs.pular != '1' + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + mkdir -p .vercel/output + cp -R /tmp/artefato/preview-*/. .vercel/output/ 2>/dev/null || true + rm -f .vercel/output/pr-numero.txt .vercel/output/pr-sha.txt + URL=$(vercel deploy --prebuilt --token "$VERCEL_TOKEN") + echo "url=$URL" >> "$GITHUB_OUTPUT" + + # `${{ }}` dentro de `run:` é substituído no TEXTO do script antes do shell existir: + # valor com aspas ou `$(...)` vira comando. Aqui os dois vêm de fora — o número + # atravessou um artefato escrito por job que rodou código do fork, e a URL é saída + # de comando. Por isso descem por ENV, que o shell trata como dado. (CodeQL pegou + # esta linha; ela estava interpolada direto.) + - name: Comenta a URL + if: steps.pr.outputs.pular != '1' + env: + GH_TOKEN: ${{ github.token }} + PR_NUM: ${{ steps.pr.outputs.numero }} + PREVIEW_URL: ${{ steps.deploy.outputs.url }} + run: | + gh pr comment "$PR_NUM" --repo "$GITHUB_REPOSITORY" \ + --body "🤖 **preview** no ar → $PREVIEW_URL" diff --git a/ARCH.generated.md b/ARCH.generated.md index 459369c07..18a60218e 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -8,9 +8,9 @@ Números atuais das zonas, do quality gate e do `package.json`: | Zona | O que é | Tamanho medido | Regra | |---|---|---|---| -| `public/` | o **jogo** | 59 arquivos `.js`, 40.891 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | -| `src/` | o **site** | 18 páginas `.astro`, 20 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | -| `tools/` | o **arnês** | 291 scripts em `tools/eval/`, 76 em `tools/` | node puro: sobe o jogo real sem browser | +| `public/` | o **jogo** | 59 arquivos `.js`, 40.552 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | +| `src/` | o **site** | 18 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | +| `tools/` | o **arnês** | 301 scripts em `tools/eval/`, 76 em `tools/` | node puro: sobe o jogo real sem browser | **Não existe `public/index.html`.** O HTML do jogo é `src/pages/index.astro`, servido na rota `/`. Servir `public/` estaticamente entrega os arnêses visuais, **não o jogo** — é a pegadinha que custa a primeira hora de todo mundo. @@ -25,10 +25,10 @@ Comandos e scripts atuais do quality gate: ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:ambience-registry eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:charhard eval:charpbr eval:motoca-visual eval:camera-grip eval:pilot-system eval:pilot-grip eval:char-thumbnail eval:asset-integrity eval:gltf-validator eval:props-acervo eval:propsuv1 eval:character-voice eval:audio-pack-character-voice eval:cinematic-ui eval:grafite-editorial eval:map-source eval:map-new eval:campo-contract eval:lajes-rooftop eval:lajes-visual eval:lajes-authored eval:lajes-spatial eval:lajes-gap eval:lajes-circuito eval:lajes-antitrap eval:mansao-water eval:mansao-garden eval:corrego-contract eval:corrego-water eval:look eval:mansao-ocean eval:wind eval:softparticles eval:escala-favela eval:escadao-contract eval:slice-abilities eval:faction-registry eval:mapview eval:devport spec:check skills:check eval:pickuparma eval:comentario eval:fixture eval:preload eval:docsautoria +npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam ``` -`package.json` tem **179 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **124 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/CHANGELOG.md b/CHANGELOG.md index f40da27de..6f7347b01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ > -**O jogo está em `2.0.0-alpha.169`.** Prerelease do semver ordena sozinho +**O jogo está em `2.0.0-alpha.179`.** Prerelease do semver ordena sozinho (`alpha` < `beta` < release), e o fluxo automático cuida do bump. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `grep VERSION public/js/version.js · node -p "require('./package.json').version"` @@ -23,6 +23,56 @@ > O conteúdo e as datas das entradas continuam intactos; só o rótulo mudou, porque chamar de > 3.3.0 um build com P0 em aberto promete ao jogador uma estabilidade que ele não tem. +## [2.0.0-alpha.179] — 2026-08-22 + +### Mudado +- fix(ci): o merge limpo do bot também leva os trailers (destrava o #406) (#425) + +## [2.0.0-alpha.178] — 2026-08-22 + +### Mudado +- fix(ci): o bot para de reprovar o PR que ele acabou de consertar (#424) + +## [2.0.0-alpha.177] — 2026-08-22 + +### Mudado +- feat(ci): o que ficou de fora do #408 — fase 5, varredura pós-release e preview sem token (#422) + +## [2.0.0-alpha.176] — 2026-08-22 + +### Mudado +- fix(ci): classify não reprova PR de fork por secret que o gatilho não pode ter (#408) + +## [2.0.0-alpha.175] — 2026-08-22 + +### Mudado +- fix(régua): prova de mordida que não morde passa a REPROVAR (#416) + +## [2.0.0-alpha.174] — 2026-08-22 + +### Mudado +- fix(regua): DOCSAUT deixa de reprovar branch de PR com autor novo (#415) + +## [2.0.0-alpha.173] — 2026-08-21 + +### Mudado +- perf(armas): a partida carrega as armas dela — 26 no bloqueante viram 9 (#410) + +## [2.0.0-alpha.172] — 2026-08-21 + +### Mudado +- fix: importmap do Layout e CVEs altas nas dependências (#363) + +## [2.0.0-alpha.171] — 2026-08-21 + +### Mudado +- feat: kill replay cam com hit-stop (#364) + +## [2.0.0-alpha.170] — 2026-08-21 + +### Mudado +- feat(tela-04): duas colunas na escolha de mapa e varredura do i18n (#401) + ## [2.0.0-alpha.169] — 2026-08-21 ### Mudado diff --git a/PROD-READINESS.md b/PROD-READINESS.md index badd352c1..42b9ea9a3 100644 --- a/PROD-READINESS.md +++ b/PROD-READINESS.md @@ -26,6 +26,35 @@ - Headers de cache: `/vendor/*` curto, `/models/*` e `/audio/a/*` longo/immutable, `og-image.png` 7 dias. - CSP está declarado em `vercel.json`; import map e scripts são `self` + `unsafe-inline` + Cloudflare beacon. +## Preview de PR e a Vercel (21/08/2026) + +Medido nos últimos 29 PRs: a Vercel reprovou **13 dos 14 PRs vindos de fork**, sempre com +`Authorization required to deploy.` - é a proteção de fork da própria Vercel, e nenhum +commit do colaborador a resolve. + +**Quem bloqueia o PR é o `ci.yml`**, que já roda `npm run build` em `pull_request`. A +Vercel é conveniência, não portão - a régua `eval:deploygate` (DG1) guarda essa condição. + +Dois ajustes ficam no **painel da Vercel**, fora do alcance do repositório, e precisam da +conta dona do projeto: + +1. **Project → Git → Deploy Hooks / Fork Protection**: desligar o deploy automático de PR + vindo de fork. Enquanto estiver ligado, todo PR externo nasce com um vermelho que o + autor não tem como consertar. +2. **Settings → Git → Ignored Build Step**: opcional, para parar de gastar build em branch + de PR interna. A produção continua publicando pela `main`. + +O preview de fork **não pede aprovação a ninguém** desde 22/08, e sem expor o token: +o `preview-build.yml` compila o código do fork em `pull_request` — que num PR de fork +roda **sem acesso a `secrets`** —, e o `preview-deploy.yml` publica em `workflow_run`, +que roda no contexto base **com** o token e **não executa nada do PR** (`vercel deploy +--prebuilt` só envia arquivo). Quem tem o que roubar não roda código de terceiro; quem +roda código de terceiro não tem o que roubar. + +O contrato está preso em `scripts/ci/workflow_security_check.py` (PRV1/PRV2/PRV3): nove +mutações, incluindo pôr `secrets.` no job que compila e um `actions/checkout` no que +publica. + ## Blockers conhecidos para prod 1. **`npm run check` (full) não foi executado nesta sessão.** `check:fast` passou; `check` ainda inclui `eval:vm`, `invariants`, `kick`, `bots` e deve ser verde antes de publicar. diff --git a/README.md b/README.md index c65fd904e..f422073f5 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,9 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 40.891 linhas em 59 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **7.188** linhas | `wc -l public/js/game.js` | -| `main.js` | 2.760 linhas | `wc -l public/js/main.js` | +| Código do jogo | 40.552 linhas em 59 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 27 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 63 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | | Props em GLB | 151 | `git ls-files 'public/models/props/*.glb' \| wc -l` | @@ -44,10 +44,10 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Personagens jogáveis | 62, em 10 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 17 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 16 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 291 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 301 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 76 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.169` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.179` | `public/js/version.js` e `package.json` (batem) | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` @@ -91,7 +91,7 @@ arquitetura): `cd docs && npm install && npm start` → Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` @@ -192,17 +192,7 @@ está lá. Use `npm run dev`. ## Quality gate de qualidade - - -```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:ambience-registry eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:charhard eval:charpbr eval:motoca-visual eval:camera-grip eval:pilot-system eval:pilot-grip eval:char-thumbnail eval:asset-integrity eval:gltf-validator eval:props-acervo eval:propsuv1 eval:character-voice eval:audio-pack-character-voice eval:cinematic-ui eval:grafite-editorial eval:map-source eval:map-new eval:campo-contract eval:lajes-rooftop eval:lajes-visual eval:lajes-authored eval:lajes-spatial eval:lajes-gap eval:lajes-circuito eval:lajes-antitrap eval:mansao-water eval:mansao-garden eval:corrego-contract eval:corrego-water eval:look eval:mansao-ocean eval:wind eval:softparticles eval:escala-favela eval:escadao-contract eval:slice-abilities eval:faction-registry eval:mapview eval:devport spec:check skills:check eval:pickuparma eval:comentario eval:fixture eval:preload eval:docsautoria -``` - -`package.json` tem **179 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. - -> Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` - - +Comandos atuais do quality gate: veja [`ARCH.generated.md`](ARCH.generated.md) (gerado — não editar à mão). ```bash npm run arch # regenera tools/eval/ARCH.md (índice + tabela de conflito) diff --git a/SCRIPTS.md b/SCRIPTS.md index a2dc44027..f989af06a 100644 --- a/SCRIPTS.md +++ b/SCRIPTS.md @@ -689,6 +689,14 @@ npm run eval:simclock BUG-57 (dono, 17/08): "precisamos disso em todos os mapas". Varre o REGISTRO em node puro: AR1 todo mapa devolve ambience, AR2 população mínima por bioma (interno = só rato), AR3 fauna fora de sólido, AR4 espécie-chave por bioma (v2.1: gato/galinha/vaca + vida 1: tatu/barata/papagaio), AR5 nenhuma pomba em modo flight, AR6 todo mapa devolve sound (vida 1, plans/22). A irmã eval:ambience (browser) mede qualidade em 3 mapas; esta garante que mapa novo não nasce morto. Mutantes: sem-ambience|fauna-em-solido|sem-gato|pomba-voa-de-novo|sem-som|sem-fauna2. +## `eval:fauna-shots` + +O par de TELA do `eval:ambience-registry`. O registry roda em node puro e prova que o mapa DEVOLVE fauna — mas com fallback procedural: ele não sabe dizer se o GLB carregou, se o mixer anda ou se o bicho reage. É a mesma classe de furo do decal de grafite (12,7% real x 334 no probe). Esta abre o jogo de verdade (`?auto=`), espera `live` e mede: FS1 `report().gltf` (todo animal veio de GLB, zero fallback), FS2 a soma dos `clipTime` AVANÇA (animação rodando), FS3 um tiro real por `Game._fireHitscan` leva o alvo a flee/takeoff em 700 ms, FS4 PNG antes/depois como evidência. FS5 imprime as espécies SEM mixer — evidência, SEM limiar: medido em 20/08, tatu/barata/papagaio nascem estáticos porque `tatu_campo.glb`, `barata_urbana.glb` e `papagaio_poleiro.glb` têm ZERO clipe no arquivo, e o conserto é asset novo, não código. Lista de mapas vem de `MAPS` (mapa novo entra sozinho) e o alvo é escolhido em tempo de execução (coordenada literal apodrece a cada remanejo de fauna). DUAS LIÇÕES PAGAS AQUI: a primeira versão comparava o snapshot INTEIRO e ficava verde com os mixers congelados, porque bicho anda por código — quem expôs foi o mutante `congela`; e a segunda usava janela fixa de 1,5 s, que reprovava upa_24h/campomorro/loja_h com +0,00s porque no headless um mapa pesado quase não apresenta quadro e mixer só anda em quadro apresentado — hoje sonda até 8 s e passa no primeiro avanço. EXIGE BROWSER E SERVIDOR (`npm run eval:serve`), então é passo de pré-deploy e não de check:fast. `--mutante=congela` (FS2) e `--mutante=surdo` (FS3) provam que morde. Estado 20/08: 17/17 mapas, FS1/FS2/FS3 verdes. + +## `eval:preload-roster` + +A PARTIDA CARREGA O ELENCO QUE ELA USA, NÃO O ELENCO INTEIRO. O `main.js` chamava `preloadCharacterAssets([...GLB_CHARS])` antes de construir o Game — os 62 personagens do jogo, BLOQUEANDO, para pôr 8 bonecos em campo. Medido no arnês: 63 GLBs de personagem baixados antes de o jogador ver qualquer coisa; hoje, 9. PL1 conta os GLBs do preload BLOQUEANTE (janela: do goto até `window.__game` existir) contra um teto de 12 — contar até o `live` mediria junto a carga tardia e daria 62 mesmo com o conserto no lugar, que foi o que aconteceu na primeira medição. PL2 cobra o RESULTADO NA CENA: zero bot sem malha GLB — cortar o preload e deixar boneco de caixa em campo é trocar espera por feiura, que é pior que a espera (só bot: o jogador é 1ª pessoa e não tem malha, e incluí-lo pintava a régua de vermelho por engano). PL3 cobra que o elenco restante CHEGUE em 10 s, porque a tecla M deixa o jogador virar qualquer personagem da facção inimiga e preload enxuto sem carga tardia quebraria a troca de time em silêncio. Os mutantes são os kill-switches reais do jogo, não monkey-patch de teste: `?preloadall=1` (`--mutante=todos`, PL1 vermelha) e `?preloadlazy=0` (`--mutante=sem-lazy`, PL3 vermelha). EXIGE BROWSER E SERVIDOR — passo de pré-deploy. + ## `eval:mapanovo` O PORTÃO DE MAPA NOVO — uma régua só para as cláusulas estruturais dos 10 mapas, varrendo o REGISTRO e não uma lista à mão. Nasceu do dono em 12/08 ('os 5 mapas novos estão low poly e injogáveis'): o gl-shots tinha lista LITERAL parada em 5 enquanto o jogo foi para 10, então escadão/campo/lajes/córrego/mansão nunca passaram por captura nenhuma — mapa que não é fotografado não é criticado, e o que não é criticado regride calado. ORT1 ortogonalidade (≥20 ângulos distintos E ≥15% de massa fora da grade, só na classe `organico`: favela é autoconstrução, mas salão de piscina e estacionamento de loja são ortogonais DE VERDADE e ficam isentos por referência); ALT1 h90 ≥ 9 m; SUP1/SUP2 cor chapada, com os tetos LIDOS do fonte do corrego-superficie-check (fonte única — dois limiares para o mesmo conceito é o instrumento discordando de si); JOG1/JOG2 consumidos do map_check.json; COB1/COB2 cobertura de captura, que é a cláusula-meta cuja ausência causou tudo isso. Não saber medir custa o mesmo que estar errado: texel-check mudo, map_check.json ausente OU MAIS VELHO que os fontes de mapa, e mapa que não constrói ficam VERMELHOS. Dívida de hoje (28 entradas, medidas e nomeadas) mora na tabela DIVIDA do próprio arquivo e AVISA em vez de reprovar; vermelho fora dela reprova. `--simular-novo=` ignora a dívida daquele mapa e responde 'se ele chegasse hoje, entrava?' — medido, fy_campomorro reprova em ORT1/SUP1/SUP2. FORA DO check:fast DE PROPÓSITO: ele consome o map_check.json e o `eval:map-new` do próprio check:fast reescreve esse JSON com só 5 mapas (map-check.mjs:153), então a ordem deixaria o portão vermelho por contabilidade, não por defeito — ele exige `node tools/eval/map-check.mjs all` antes e é passo de rodada/pré-deploy. `--mutante=grade-perfeita|teto-baixo|tudo-chapado|texel-ausente|corpo-dentro|mapcheck-velho|registro-fora-de-forma|bateria-fixa` provam que morde, um por cláusula. diff --git a/STATUS.md b/STATUS.md index 9816c367a..b93ad7cdc 100644 --- a/STATUS.md +++ b/STATUS.md @@ -2,10 +2,10 @@ -- **Versão:** `2.0.0-alpha.169` +- **Versão:** `2.0.0-alpha.179` - **Conteúdo jogável:** 10 facções, 62 personagens, 17 mapas e 27 armas com GLB -- **Código do jogo:** 40.891 linhas em 59 módulos JavaScript -- **Automação:** 179 comandos npm, 291 scripts de avaliação e 76 scripts de pipeline +- **Código do jogo:** 40.552 linhas em 59 módulos JavaScript +- **Automação:** 124 comandos npm, 301 scripts de avaliação e 76 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/LICOES.md b/docs/LICOES.md index 4f34642c2..7c8067620 100644 --- a/docs/LICOES.md +++ b/docs/LICOES.md @@ -212,10 +212,47 @@ assado do grafite e para qualquer lista que um gerador reescreva. --- +## 15. Preload carrega o que a cena usa, e quem corta a espera responde pela cena + +`main.js` chamava `preloadCharacterAssets([...GLB_CHARS])` antes de construir o +Game: **62 personagens, bloqueando**, para pôr 8 bonecos em campo. Medido pela +`eval:preload-roster`: 63 GLBs baixados antes de o jogador ver qualquer coisa. + +Três coisas que o conserto ensinou, e que valem para qualquer preload desta base: + +- **Quem preloada tem que saber o que a cena vai usar.** O elenco é sorteado por + `pickMatchRoster` (game.js) e passado pronto ao Game em `matchRoster`. Sortear + DUAS vezes daria elencos diferentes e o preload erraria o alvo **em silêncio** + — a classe de defeito da lição 5. +- **Carga tardia espera a cena ficar de pé.** Soltar o resto junto da contagem + regressiva rouba banda e decode do primeiro segundo jogável, e a régua continuou + vermelha em 62 igual a antes do conserto. Hoje espera o `live`. +- **Quem corta a espera responde pelo que aparece.** `_switchTeam` (tecla M) + sorteava qualquer personagem da facção; com preload enxuto isso poria um boneco + de caixa procedural em campo. Trocar espera por feiura é pior que a espera — + hoje o sorteio prefere quem tem GLB em memória. É a cláusula PL2. + +Resultado: 63 → 9 GLBs no preload bloqueante. Régua, cláusulas e mutantes +(`?preloadall=1`, `?preloadlazy=0`) em `SCRIPTS.md`, `eval:preload-roster`. + +### Equilíbrio de times (o que já morava no `game.js`) + +`cycle` devolve `[]` quando o pool é VAZIO — `pool[i % 0]` é NaN → undefined e o +`.filter(Boolean)` limpa tudo. Uma facção sem personagens suficientes produzia um +time MENOR **sem nenhum aviso** (jogador sozinho contra 8). Hoje as facções têm +8-9 personagens e a conta fecha — medido, enumerando as 16 combinações +facção×inimigo × teamSize 1..8: todas dão N vs N. O bug é LATENTE: basta uma +facção entrar com 1 personagem para ele voltar, de novo em silêncio. `roster` +fecha isso: SEMPRE devolve `want` combatentes (repetir personagem é aceitável; +time menor não é) e AVISA no console quando repetiu ou recorreu ao elenco geral. + +--- + ## Como usar este arquivo - **Antes de escrever régua:** leia 1, 2, 3, 4. - **Antes de mexer em asset ou build:** leia 5, 11, 12, 14. +- **Antes de mexer em preload ou em quem entra na cena:** leia 15. - **Antes de gerar arte com pessoa real:** leia 9. - **Quando o portão estiver verde e o dono disser que está errado:** leia 1 e 3. Esse caso é o mais importante desta base, e o mais mal resolvido. diff --git a/docs/docs/arquitetura.md b/docs/docs/arquitetura.md index ebf8e646d..5220077b7 100644 --- a/docs/docs/arquitetura.md +++ b/docs/docs/arquitetura.md @@ -62,15 +62,15 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | Arquivo | Linhas | |---|---:| -| `public/js/game.js` | 7.188 | -| `public/js/main.js` | 2.760 | +| `public/js/game.js` | 6.910 | +| `public/js/main.js` | 2.698 | | `public/js/characters.js` | 1.168 | -| `public/js/glbchars.js` | 960 | +| `public/js/glbchars.js` | 844 | | `public/js/vmattach.js` | 628 | -| `public/js/weapons.js` | 353 | +| `public/js/weapons.js` | 346 | | `public/js/springs.js` | 260 | -Total de `public/js/`: **40.891 linhas em 59 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). +Total de `public/js/`: **40.552 linhas em 59 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: ``git ls-files public/js/*.js | xargs wc -l`` @@ -251,7 +251,7 @@ versão do `docs:check`, e é o mesmo modo de falha do BUG-02 (o quality gate me de ontem porque o `&&` cortava antes de o JSON ser regenerado). Por isso o `docs:check` vem **antes** do `arch:check` no `package.json`, com o motivo -escrito na chave `//check:fast`. Quando o `ARCH.md` for regenerado e o `arch:check` voltar +escrito no `SCRIPTS.md` (chave `check:fast`). Quando o `ARCH.md` for regenerado e o `arch:check` voltar a verde, a ordem deixa de importar; até lá, importa. ::: diff --git a/docs/docs/colaborar.md b/docs/docs/colaborar.md index 9647abd04..9016a8725 100644 --- a/docs/docs/colaborar.md +++ b/docs/docs/colaborar.md @@ -14,7 +14,7 @@ O número abaixo não é retórica, e não é escrito à mão: sai de `git short {/* BEGIN:GERADO:pessoas — não edite à mão, rode `npm run docs` */} -**11 identidades de autoria humana** assinam commit no histórico **desta branch**: `ruben-cytonic`, `Ruben Marcus`, `Emerson Garrido`, `Ruben`, `rubenmarcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém **não aparece aqui**. +**11 identidades de autoria humana** assinam commit no histórico **desta branch**: `ruben-cytonic`, `Ruben Marcus`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém **não aparece aqui**. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `git shortlog -sn --no-merges (descontando autores que são agentes)` @@ -30,7 +30,7 @@ qualquer decisão de licença está no `CONTRIBUTING.md` (seção de licença e ::: Isso é relevante pra você de duas formas opostas. A ruim: se o seu PR travar, pode -demorar. A boa: **quase toda a régua é máquina.** `npm run check` te dá o mesmo veredito +demorar. A boa: **quase toda a régua é máquina.** `npm run check:fast` te dá o mesmo veredito que o mantenedor daria, antes de você abrir o PR, sem esperar ninguém. A barreira é baixa **de propósito** — é um dos princípios que não mudam do [`docs/ROADMAP.md`](https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md). @@ -68,7 +68,7 @@ Use `npm run dev`. Detalhes e prova em npm run eval:vm # OBRIGATÓRIO ANTES — ver o aviso abaixo node tools/eval/invariants.mjs # o quality gate inteiro node tools/eval/invariants.mjs --json # saída pra máquina -npm run check # syntax + vm + quality gate + coice + bots +npm run check:fast # syntax + quality gate (réguas de node puro) ``` :::danger `eval:vm` roda ANTES de `invariants.mjs`. Sempre. @@ -347,9 +347,12 @@ em `git ls-files public/models/anims`). Doc que manda fazer o que já foi feito primeira contribuição de alguém; por isso a lista virou ponteiro para `docs/issues/`, que é mantida. -O único item da lista antiga que **continua valendo**: a mensagem das invariantes -PX1–PX4 manda usar `tools/eval/motion.mjs`, que não existe (`ls` confirma). Apontar para -o arnês certo, ou marcar como "arnês a escrever", é um PR de 15 minutos. +O único item da lista antiga que **continua valendo** — e agora está consertado: +a mensagem das invariantes PX1–PX4 apontava para `tools/eval/motion.mjs`, que +nunca existiu no git (ponteiro fantasma). Hoje as skips declaram honestamente +"sem arnês dedicado (dívida PX)": o que existe de browser no CI é o +`portao-browser` (boot real do jogo + grafite + silhueta da seleção), e um +arnês de viewmodel dedicado continua sendo trabalho aberto. ::: ### Trabalho de verdade, ainda acessível diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index e5b1521c3..c4094112c 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,9 +39,9 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 40.891 linhas em 59 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **7.188** linhas | `wc -l public/js/game.js` | -| `main.js` | 2.760 linhas | `wc -l public/js/main.js` | +| Código do jogo | 40.552 linhas em 59 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 27 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 63 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | | Props em GLB | 151 | `git ls-files 'public/models/props/*.glb' \| wc -l` | @@ -49,10 +49,10 @@ esta página envelhecia no primeiro commit — ver | Personagens jogáveis | 62, em 10 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 17 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 16 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 291 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 301 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 76 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.169` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.179` | `public/js/version.js` e `package.json` (batem) | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` @@ -280,19 +280,20 @@ E os dois quality gates, com a lista exata do que cada um roda — direto do `pa {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:ambience-registry eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:charhard eval:charpbr eval:motoca-visual eval:camera-grip eval:pilot-system eval:pilot-grip eval:char-thumbnail eval:asset-integrity eval:gltf-validator eval:props-acervo eval:propsuv1 eval:character-voice eval:audio-pack-character-voice eval:cinematic-ui eval:grafite-editorial eval:map-source eval:map-new eval:campo-contract eval:lajes-rooftop eval:lajes-visual eval:lajes-authored eval:lajes-spatial eval:lajes-gap eval:lajes-circuito eval:lajes-antitrap eval:mansao-water eval:mansao-garden eval:corrego-contract eval:corrego-water eval:look eval:mansao-ocean eval:wind eval:softparticles eval:escala-favela eval:escadao-contract eval:slice-abilities eval:faction-registry eval:mapview eval:devport spec:check skills:check eval:pickuparma eval:comentario eval:fixture eval:preload eval:docsautoria +npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam ``` -`package.json` tem **179 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **124 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` {/* END:GERADO:scripts */} -O `npm run check` é o mesmo conjunto que o CI roda em `.github/workflows/ci.yml`. +O `check:fast` cobre as réguas de node puro; o CI (`.github/workflows/ci.yml`) roda o +mesmo conjunto mais os passos que exigem rede. -:::tip Use o `check:fast` no loop, o `check` antes do PR -O `check` gasta 10-12 min porque sobe o jogo cinco vezes. O `check:fast` cobre as réguas +:::tip Use o `check:fast` no loop, o `portao-browser` antes do PR +O `portao-browser` gasta 10-12 min porque sobe o jogo cinco vezes. O `check:fast` cobre as réguas que nasceram dos bugs mais recentes (menu de pausa, rodada de captura, regeneração, manifesto de animação) e roda em cerca de um minuto. ::: diff --git a/docs/docs/quality-gates.md b/docs/docs/quality-gates.md index c05337d37..011e25991 100644 --- a/docs/docs/quality-gates.md +++ b/docs/docs/quality-gates.md @@ -14,8 +14,8 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} -- `tools/eval/invariants.mjs`: **2.281 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. -- O arnês inteiro são **291 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **76 scripts** de pipeline em `tools/`. +- `tools/eval/invariants.mjs`: **2.275 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. +- O arnês inteiro são **301 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **76 scripts** de pipeline em `tools/`. - Quantas invariantes rodam como **críticas** numa execução **não é derivável do fonte**: depende de qual insumo existe na máquina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse número só sai rodando o quality gate — e o lugar dele é o cabeçalho do `KNOWN-BUGS.md`, atualizado com saída real. Reproduza: diff --git a/docs/docs/stack.md b/docs/docs/stack.md index d74bbf849..292245918 100644 --- a/docs/docs/stack.md +++ b/docs/docs/stack.md @@ -19,7 +19,7 @@ a partir do `package.json`, do `docs/package.json` e do próprio Three.js vendor | Motor 3D (WebGL) | **Three.js**, vendorizado | `r160` | | Jogo | ES modules vanilla, **zero build** | 59 arquivos | | Site | **Astro** com SSR | `^7.1.1` | -| Hospedagem | adapter **Vercel** | `^11.0.3` | +| Hospedagem | adapter **Vercel** | `^11.0.6` | | Banco | **Postgres gerenciado** (RLS; schema privado, fora do repo) | `^2.110.7` | | Browser nas réguas | **Playwright** | `^1.62.1` | | Pipeline de GLB | **gltf-transform** | `^4.4.1` | @@ -28,7 +28,7 @@ a partir do `package.json`, do `docs/package.json` e do próprio Three.js vendor | Esta documentação | **Docusaurus** | `3.6.3` | | Runtime de CI | **Node** | `22` | -Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **140** importam Playwright, **58** importam gltf-transform e **5** importam meshoptimizer. +Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **143** importam Playwright, **58** importam gltf-transform e **5** importam meshoptimizer. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` @@ -254,9 +254,9 @@ de trabalhar. Elas vivem em `.agents/skills/`, e `.claude/skills/` são symlinks `.claude/skills/` são **symlinks** para `.agents/skills/` — uma cópia só, dois nomes, porque o Claude Code lê de `.claude/` e outros arnêses leem de `.agents/`. -⚠️ Nenhuma skill nativa encontrada em `.claude/skills/`. +A skill do loop desta casa, **`gauntlet-fps`**, é a única que nasceu aqui: vive em `.claude/skills/gauntlet-fps/SKILL.md`, não é symlink e não entra no lock. -> Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `git ls-files .agents/skills · git ls-files .claude/skills · skills-lock.json` +> Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `git ls-files .agents/skills · skills-lock.json` {/* END:GERADO:skills */} diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md index 7d7248a56..c9887e159 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md @@ -69,15 +69,15 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | File | Lines | |---|---:| -| `public/js/game.js` | 7,188 | -| `public/js/main.js` | 2,760 | +| `public/js/game.js` | 6,910 | +| `public/js/main.js` | 2,698 | | `public/js/characters.js` | 1,168 | -| `public/js/glbchars.js` | 960 | +| `public/js/glbchars.js` | 844 | | `public/js/vmattach.js` | 628 | -| `public/js/weapons.js` | 353 | +| `public/js/weapons.js` | 346 | | `public/js/springs.js` | 260 | -Total in `public/js/`: **40,891 lines in 59 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. +Total in `public/js/`: **40,552 lines in 59 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. > Block generated by `node tools/gen-docs.mjs`. Source: ``wc -l public/js/*.js`` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md index 801ed7e0c..861f4d442 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md @@ -17,7 +17,7 @@ The number below is not rhetoric, and it is not hand-written: it comes from `git {/* BEGIN:GERADO:pessoas — não edite à mão, rode `npm run docs` */} -**11 human author identities** sign commits in this branch: `ruben-cytonic`, `Ruben Marcus`, `Emerson Garrido`, `Ruben`, `rubenmarcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. Automated identities are excluded. A Git author name is not necessarily one unique person. +**11 human author identities** sign commits in this branch: `ruben-cytonic`, `Ruben Marcus`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. Automated identities are excluded. A Git author name is not necessarily one unique person. > Block generated by `node tools/gen-docs.mjs`. Source: `git shortlog -sn --no-merges (descontando autores que são agentes)` @@ -350,9 +350,12 @@ in `git ls-files public/models/anims`). A doc that tells you to do what has alre someone's first contribution; that is why the list became a pointer to `docs/issues/`, which is maintained. -The only item from the old list that **still stands**: the message of invariants -PX1–PX4 tells you to use `tools/eval/motion.mjs`, which does not exist (`ls` confirms). Pointing to -the right harness, or marking it as "harness to be written", is a 15-minute PR. +The only item from the old list that **still stands** — and is now fixed: the +message of invariants PX1–PX4 pointed to `tools/eval/motion.mjs`, which never +existed in git (a phantom pointer). The skips now honestly declare "no dedicated +harness (PX debt)": what runs in CI browsers today is `portao-browser` (real game +boot + graffiti + selection-screen silhouette), and a dedicated viewmodel +harness remains open work. ::: ### Real work, still accessible {#real-work-still-accessible} diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 0dd13f49d..a60c84d11 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,9 +41,9 @@ this page was aging at the very first commit — see | What | How much | Where to check | |---|---:|---| -| Game code | 40,891 lines in 59 files | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **7,188** lines | `wc -l public/js/game.js` | -| `main.js` | 2,760 lines | `wc -l public/js/main.js` | +| Game code | 40,552 lines in 59 files | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6,910** lines | `wc -l public/js/game.js` | +| `main.js` | 2,698 lines | `wc -l public/js/main.js` | | Weapons with GLB | 27 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | Character GLBs | 63 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | | Props in GLB | 151 | `git ls-files 'public/models/props/*.glb' \| wc -l` | @@ -51,10 +51,10 @@ this page was aging at the very first commit — see | Playable characters | 62, in 10 factions | `CHARACTERS` array in `characters.js` | | Maps in the registry | 17 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 16 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 291 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 301 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 76 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Version | `2.0.0-alpha.169` | `public/js/version.js` and `package.json` (match) | +| Version | `2.0.0-alpha.179` | `public/js/version.js` and `package.json` (match) | > Block generated by `node tools/gen-docs.mjs`. Source: `the command in the right column of each row` @@ -282,10 +282,10 @@ And the two gates, with the exact list of what each one runs — straight from ` {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:ambience-registry eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:charhard eval:charpbr eval:motoca-visual eval:camera-grip eval:pilot-system eval:pilot-grip eval:char-thumbnail eval:asset-integrity eval:gltf-validator eval:props-acervo eval:propsuv1 eval:character-voice eval:audio-pack-character-voice eval:cinematic-ui eval:grafite-editorial eval:map-source eval:map-new eval:campo-contract eval:lajes-rooftop eval:lajes-visual eval:lajes-authored eval:lajes-spatial eval:lajes-gap eval:lajes-circuito eval:lajes-antitrap eval:mansao-water eval:mansao-garden eval:corrego-contract eval:corrego-water eval:look eval:mansao-ocean eval:wind eval:softparticles eval:escala-favela eval:escadao-contract eval:slice-abilities eval:faction-registry eval:mapview eval:devport spec:check skills:check eval:pickuparma eval:comentario eval:fixture eval:preload eval:docsautoria +npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam ``` -`package.json` has **179 scripts**; the reason behind each one lives in `SCRIPTS.md`. +`package.json` has **124 scripts**; the reason behind each one lives in `SCRIPTS.md`. > Block generated by `node tools/gen-docs.mjs`. Source: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md index 1f132b044..624403239 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md @@ -17,8 +17,8 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} -- `tools/eval/invariants.mjs`: **2,281 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. -- The harness contains **291 scripts** in `tools/eval/`, plus **76 pipeline scripts** in `tools/`. +- `tools/eval/invariants.mjs`: **2,275 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. +- The harness contains **301 scripts** in `tools/eval/`, plus **76 pipeline scripts** in `tools/`. - The number of critical checks in one run depends on the inputs present on that machine; dated results belong in `KNOWN-BUGS.md`. ```bash diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md index f50160226..a90fa21ce 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md @@ -22,7 +22,7 @@ from `package.json`, `docs/package.json` and the vendored Three.js itself. | 3D engine (WebGL) | **Three.js**, vendored | `r160` | | Game | vanilla ES modules, **zero build** | 59 files | | Site | **Astro** with SSR | `^7.1.1` | -| Hosting | **Vercel** adapter | `^11.0.3` | +| Hosting | **Vercel** adapter | `^11.0.6` | | Database | **managed Postgres** (RLS; private schema) | `^2.110.7` | | Browser checks | **Playwright** | `^1.62.1` | | GLB pipeline | **gltf-transform** | `^4.4.1` | @@ -31,7 +31,7 @@ from `package.json`, `docs/package.json` and the vendored Three.js itself. | This documentation | **Docusaurus** | `3.6.3` | | CI runtime | **Node** | `22` | -Three.js comes from `public/vendor/three.module.js`. Of the scripts in `tools/`, **140** import Playwright, **58** import gltf-transform, and **5** import meshoptimizer. +Three.js comes from `public/vendor/three.module.js`. Of the scripts in `tools/`, **143** import Playwright, **58** import gltf-transform, and **5** import meshoptimizer. > Block generated by `node tools/gen-docs.mjs`. Source: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` @@ -251,9 +251,9 @@ working. They live in `.agents/skills/`, and `.claude/skills/` are symlinks to t The counts differ by design: the lock records more third-party skills than the repository vendors. -The `gauntlet-fps` skill was not found. +The house workflow skill, **`gauntlet-fps`**, is present locally and does not belong to the third-party lock. -> Block generated by `node tools/gen-docs.mjs`. Source: `git ls-files .agents/skills · git ls-files .claude/skills · skills-lock.json` +> Block generated by `node tools/gen-docs.mjs`. Source: `git ls-files .agents/skills · skills-lock.json` {/* END:GERADO:skills */} diff --git a/package-lock.json b/package-lock.json index 75aca4693..45aed4542 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,21 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.169", + "version": "2.0.0-alpha.179", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coro-solto", - "version": "2.0.0-alpha.169", + "version": "2.0.0-alpha.179", "license": "AGPL-3.0-only", "dependencies": { - "@astrojs/vercel": "^11.0.3", + "@astrojs/vercel": "^11.0.6", "@resvg/resvg-js": "^2.6.2", "@resvg/resvg-wasm": "^2.6.2", "@supabase/supabase-js": "^2.110.7", "astro": "^7.1.1", "dejavu-fonts-ttf": "^2.37.3", + "ndarray-pixels": "^5.2.0", "sharp": "^0.35.3" }, "devDependencies": { @@ -262,12 +263,12 @@ } }, "node_modules/@astrojs/vercel": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-11.0.5.tgz", - "integrity": "sha512-cag6EfSclaXiya6R7TT52m+IechBXt+m57wtj1L0ijGJg8cToN3Tuhj9o+JBGk+IaH0wp1Eh/Qlc2NZKs/Q3HA==", + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-11.0.6.tgz", + "integrity": "sha512-GW+2Sw0qWnpuAa2dFqid8nmdSPWw3ZhF+GXxLZkyAl//H+bzNsG0N5kYXO9UU88jtDH91igYqQE1oUq1AJrOZA==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.10.2", + "@astrojs/internal-helpers": "0.10.3", "@vercel/analytics": "^1.6.1", "@vercel/functions": "^3.4.3", "@vercel/nft": "^1.3.2", @@ -280,9 +281,9 @@ } }, "node_modules/@astrojs/vercel/node_modules/@astrojs/internal-helpers": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.2.tgz", - "integrity": "sha512-yt7fMgPYqSM4Tmr+taTW6Per+hjJ8Pk6lA1PAcDyqzOt8HzJ6Kje5WzCxA2Sd+9wsUW7uhkLeoTMK0cXPwH9rQ==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.3.tgz", + "integrity": "sha512-HIx/t1NywcqSTFaVwZh15n8JsC3YQdCaJZDaTS/aurYTEvNiWDXUGS3Z9uQX3bFB+B1pCgN/nljckkzYTpHZ2w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.4", @@ -2358,7 +2359,6 @@ "version": "1.0.14", "resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.0.14.tgz", "integrity": "sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==", - "dev": true, "license": "MIT" }, "node_modules/@types/nlcst": { @@ -3073,7 +3073,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz", "integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==", - "dev": true, "license": "MIT", "dependencies": { "uniq": "^1.0.0" @@ -3682,7 +3681,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==", - "dev": true, "license": "MIT" }, "node_modules/iron-webcrypto": { @@ -3698,7 +3696,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, "license": "MIT" }, "node_modules/is-docker": { @@ -4286,7 +4283,6 @@ "version": "1.0.19", "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz", "integrity": "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==", - "dev": true, "license": "MIT", "dependencies": { "iota-array": "^1.0.0", @@ -4308,548 +4304,21 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/ndarray-ops/-/ndarray-ops-1.2.2.tgz", "integrity": "sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==", - "dev": true, "license": "MIT", "dependencies": { "cwise-compiler": "^1.0.0" } }, "node_modules/ndarray-pixels": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.0.1.tgz", - "integrity": "sha512-IBtrpefpqlI8SPDCGjXk4v5NV5z7r3JSuCbfuEEXaM0vrOJtNGgYUa4C3Lt5H+qWdYF4BCPVFsnXhNC7QvZwkw==", - "dev": true, + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.2.0.tgz", + "integrity": "sha512-lTh4tFKziAatVTa9crIsidUyn+lqujVOQpzfdBWvdFu2wo9Uo6z261lVX7SgMyP89xGmj3TMTPbbxl9YDnV4SA==", "license": "MIT", "dependencies": { "@types/ndarray": "^1.0.14", "ndarray": "^1.0.19", "ndarray-ops": "^1.2.2", - "sharp": "^0.34.0" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "sharp": "^0.35.0" } }, "node_modules/neotraverse": { @@ -5123,9 +4592,9 @@ } }, "node_modules/path-to-regexp": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", - "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, "node_modules/path-to-regexp-updated": { @@ -5759,7 +5228,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==", - "dev": true, "license": "MIT" }, "node_modules/unist-util-is": { diff --git a/package.json b/package.json index 1355cf5a3..78cdccfdf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.169", + "version": "2.0.0-alpha.179", "license": "AGPL-3.0-only", "description": "CORO SOLTO: Treta Suprema (ex-CS BRASIL) — FPS satírico de navegador com 5 facções brasileiras (site Astro + jogo vanilla em Three.js), gerado em par com agentes de IA (AI generated & AI friendly)", "scripts": { @@ -70,6 +70,8 @@ "bot:brain:check": "node tools/eval/bot-brain-check.mjs", "eval:botbrain": "node tools/eval/botbrain-safety-check.mjs", "eval:preload": "node tools/eval/preload-check.mjs", + "eval:armas": "node tools/eval/armas-check.mjs", + "eval:comentario": "node tools/eval/comentario-check.mjs", "eval:posters": "node tools/eval/poster-aspect-check.mjs", "eval:ui": "node tools/eval/ui-check.mjs all", "eval:redesign": "node tools/eval/redesign-check.mjs", @@ -86,6 +88,7 @@ "eval:pause": "node tools/eval/pause-check.mjs", "eval:select": "node tools/eval/select-inflate.mjs", "eval:entrada": "node tools/eval/entrada-check.mjs", + "eval:replaycam": "node tools/eval/replaycam-check.mjs", "eval:select:mutate": "node tools/eval/select-inflate.mjs mandrake,pagodeiro --mutate=skin", "assert:assets": "node tools/eval/assets-check.mjs", "strip:decalbg": "node tools/strip-decal-bg.mjs", @@ -117,7 +120,6 @@ "eval:regen": "node tools/eval/regen-check.mjs", "eval:ctfwin": "node tools/eval/ctf-win-check.mjs", "eval:spawn": "node tools/eval/spawn-settle-check.mjs", - "eval:docsautoria": "node tools/eval/docs-autoria-check.mjs", "eval:devport": "node tools/eval/dev-port-check.mjs", "eval:mitico": "node tools/eval/mythic-character-check.mjs", "eval:deathcam": "node tools/eval/death-camera-check.mjs", @@ -152,6 +154,8 @@ "eval:lajes-authored": "node tools/eval/lajes-authored-check.mjs", "eval:lajes-spatial": "node tools/eval/lajes-spatial-check.mjs", "eval:ambience": "node tools/eval/ambience-check.mjs", + "eval:fauna-shots": "node tools/eval/fauna-shots.mjs", + "eval:preload-roster": "node tools/eval/preload-roster-check.mjs", "eval:mansao-water": "node tools/eval/mansao-water-check.mjs", "eval:mansao-garden": "node tools/eval/mansao-garden-check.mjs", "eval:look": "node tools/eval/look-check.mjs", @@ -182,8 +186,12 @@ "eval:deps": "node tools/eval/deps-check.mjs", "eval:pickuparma": "node tools/eval/pickup-arma-check.mjs", "eval:fixture": "node tools/eval/fixture-check.mjs", - "eval:comentario": "node tools/eval/comentario-check.mjs", - "eval:propsuv1": "node tools/eval/props-uv1-check.mjs" + "eval:propsuv1": "node tools/eval/props-uv1-check.mjs", + "eval:mutcega": "node tools/eval/mutacao-cega-check.mjs", + "eval:wfsecret": "node tools/eval/workflow-secret-check.mjs", + "eval:deploygate": "node tools/eval/deploy-gate-check.mjs", + "eval:autofix": "node tools/eval/autofix-check.mjs", + "eval:portaointeiro": "node tools/eval/portao-inteiro-check.mjs" }, "repository": { "type": "git", @@ -194,16 +202,22 @@ }, "homepage": "https://www.csbrasil.online", "dependencies": { - "@astrojs/vercel": "^11.0.3", + "@astrojs/vercel": "^11.0.6", "@resvg/resvg-js": "^2.6.2", "@resvg/resvg-wasm": "^2.6.2", "@supabase/supabase-js": "^2.110.7", "astro": "^7.1.1", "dejavu-fonts-ttf": "^2.37.3", + "ndarray-pixels": "^5.2.0", "sharp": "^0.35.3" }, "private": true, "type": "module", + "overrides": { + "@vercel/routing-utils": { + "path-to-regexp": "^6.3.0" + } + }, "devDependencies": { "@gltf-transform/core": "^4.4.1", "@gltf-transform/extensions": "^4.4.1", diff --git a/public/docs/404.html b/public/docs/404.html index 2025fdb7c..3ac092f1c 100644 --- a/public/docs/404.html +++ b/public/docs/404.html @@ -4,7 +4,7 @@ CORO SOLTO — Docs do Dev - + diff --git a/public/docs/arquitetura/index.html b/public/docs/arquitetura/index.html index 12782bda6..2f8330363 100644 --- a/public/docs/arquitetura/index.html +++ b/public/docs/arquitetura/index.html @@ -4,7 +4,7 @@ Arquitetura: N agentes no mesmo arquivo | CORO SOLTO — Docs do Dev - + @@ -37,8 +37,8 @@

Os arq

Tamanho dos arquivos que o gen-arch.mjs indexa — bloco gerado, regenerado por npm run docs e conferido por npm run docs:check:

-
ArquivoLinhas
public/js/game.js6.838
public/js/main.js2.646
public/js/characters.js1.068
public/js/glbchars.js837
public/js/vmattach.js628
public/js/weapons.js344
public/js/springs.js260
-

Total de public/js/: 31.744 linhas em 44 arquivos. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: tools/eval/ARCH.md (npm run arch).

+
ArquivoLinhas
public/js/game.js6.910
public/js/main.js2.698
public/js/characters.js1.068
public/js/glbchars.js844
public/js/vmattach.js628
public/js/weapons.js346
public/js/springs.js260
+

Total de public/js/: 32.001 linhas em 44 arquivos. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: tools/eval/ARCH.md (npm run arch).

Bloco gerado por node tools/gen-docs.mjs. Fonte: git ls-files public/js/*.js | xargs wc -l

diff --git a/public/docs/assets/js/1be81749.06cc6d24.js b/public/docs/assets/js/1be81749.06cc6d24.js new file mode 100644 index 000000000..0f72829c5 --- /dev/null +++ b/public/docs/assets/js/1be81749.06cc6d24.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[568],{4208(e,s,r){r.r(s),r.d(s,{assets:()=>t,contentTitle:()=>c,default:()=>j,frontMatter:()=>i,metadata:()=>a,toc:()=>l});const a=JSON.parse('{"id":"comecando","title":"O que \xe9, e como rodar","description":"O que \xe9 o CORO SOLTO, como rodar em 3 comandos e a estrutura real do reposit\xf3rio \u2014 conferida contra o c\xf3digo.","source":"@site/docs/comecando.md","sourceDirName":".","slug":"/","permalink":"/docs/","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/comecando.md","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"id":"comecando","title":"O que \xe9, e como rodar","sidebar_label":"Come\xe7ando","sidebar_position":1,"slug":"/","description":"O que \xe9 o CORO SOLTO, como rodar em 3 comandos e a estrutura real do reposit\xf3rio \u2014 conferida contra o c\xf3digo."},"sidebar":"dev","next":{"title":"Stack e ferramentas","permalink":"/docs/stack"}}');var d=r(4848),o=r(8453),n=r(6025);const i={id:"comecando",title:"O que \xe9, e como rodar",sidebar_label:"Come\xe7ando",sidebar_position:1,slug:"/",description:"O que \xe9 o CORO SOLTO, como rodar em 3 comandos e a estrutura real do reposit\xf3rio \u2014 conferida contra o c\xf3digo."},c="O que \xe9, e como rodar",t={},l=[{value:"Rodar em 3 comandos",id:"rodar-em-3-comandos",level:2},{value:"Linux, WebGL e modo compatibilidade",id:"linux-webgl-e-modo-compatibilidade",level:3},{value:"Alternativa sem Astro (zero depend\xeancia de build)",id:"alternativa-sem-astro-zero-depend\xeancia-de-build",level:3},{value:"A pegadinha que custa a primeira hora de todo mundo",id:"a-pegadinha-que-custa-a-primeira-hora-de-todo-mundo",level:2},{value:"Estrutura real do reposit\xf3rio",id:"estrutura-real-do-reposit\xf3rio",level:2},{value:"As duas zonas",id:"as-duas-zonas",level:3},{value:"Comandos que voc\xea vai usar",id:"comandos-que-voc\xea-vai-usar",level:2},{value:"Onde ir agora",id:"onde-ir-agora",level:2}];function h(e){const s={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,d.jsxs)(d.Fragment,{children:["\n",(0,d.jsx)("div",{className:"cs-hero",children:(0,d.jsx)("img",{className:"cs-hero__bird",src:(0,n.Ay)("/img/canarinho-header.webp"),alt:"CORO SOLTO: Treta Suprema \u2014 o canarinho, mascote do jogo, girando",width:"604",height:"240"})}),"\n",(0,d.jsx)(s.header,{children:(0,d.jsx)(s.h1,{id:"o-que-\xe9-e-como-rodar",children:"O que \xe9, e como rodar"})}),"\n",(0,d.jsxs)(s.p,{children:[(0,d.jsx)(s.strong,{children:"CORO SOLTO: Treta Suprema"})," (ex-CS BRASIL) \xe9 um FPS de navegador escrito em\nJavaScript vanilla sobre Three.js r160, no estilo do Counter-Strike 1.6: rounds,\nbots, AWP, placar por Tab, r\xe1dio de voz. Roda num link, sem instalar nada."]}),"\n",(0,d.jsxs)(s.p,{children:["Os n\xfameros abaixo ",(0,d.jsx)(s.strong,{children:"n\xe3o s\xe3o escritos \xe0 m\xe3o"}),": eles s\xe3o regerados por\n",(0,d.jsx)(s.code,{children:"node tools/gen-docs.mjs"})," a partir do c\xf3digo, e ",(0,d.jsx)(s.code,{children:"npm run docs:check"})," (dentro do\n",(0,d.jsx)(s.code,{children:"check:fast"}),") reprova o quality gate quando qualquer um deles diverge da \xe1rvore. Antes disso\nesta p\xe1gina envelhecia no primeiro commit \u2014 ver\n",(0,d.jsx)(s.a,{href:"/docs/arquitetura#o-que-%C3%A9-gerado-e-o-que-n%C3%A3o-%C3%A9",children:"o que \xe9 gerado, e o que n\xe3o \xe9"}),"."]}),"\n","\n",(0,d.jsxs)(s.table,{children:[(0,d.jsx)(s.thead,{children:(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.th,{children:"O que"}),(0,d.jsx)(s.th,{style:{textAlign:"right"},children:"Quanto"}),(0,d.jsx)(s.th,{children:"Onde confere"})]})}),(0,d.jsxs)(s.tbody,{children:[(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"C\xf3digo do jogo"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"32.001 linhas em 44 arquivos"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"git ls-files public/js/*.js | xargs wc -l"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"game.js"})}),(0,d.jsxs)(s.td,{style:{textAlign:"right"},children:[(0,d.jsx)(s.strong,{children:"6.910"})," linhas"]}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"wc -l public/js/game.js"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"main.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"2.698 linhas"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"wc -l public/js/main.js"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Armas com GLB"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"26"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"git ls-files 'public/models/weapons/*.glb' | wc -l"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"GLBs de personagem"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"45"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"git ls-files 'public/models/characters/*.glb' | wc -l"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Props em GLB"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"108"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"git ls-files 'public/models/props/*.glb' | wc -l"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Clipes de anima\xe7\xe3o versionados"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"573"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"git ls-files public/models/anims | wc -l"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Personagens jog\xe1veis"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"44, em 5 fac\xe7\xf5es"}),(0,d.jsxs)(s.td,{children:["array ",(0,d.jsx)(s.code,{children:"CHARACTERS"})," de ",(0,d.jsx)(s.code,{children:"characters.js"})]})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Mapas no registro"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"12"}),(0,d.jsxs)(s.td,{children:["objeto ",(0,d.jsx)(s.code,{children:"MAPS"})," de ",(0,d.jsx)(s.code,{children:"maps.js"})]})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Arn\xeases visuais em HTML"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"15"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"git ls-files 'public/*.html' | wc -l"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Scripts do arn\xeas"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"200"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' | wc -l"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Scripts de pipeline"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"54"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"git ls-files 'tools/*.mjs' | wc -l"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Tarefas de entrada escritas"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"26"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"git ls-files 'docs/issues/[0-9]*.md' | wc -l"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Vers\xe3o"}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:(0,d.jsx)(s.code,{children:"2.0.0-alpha.179"})}),(0,d.jsxs)(s.td,{children:[(0,d.jsx)(s.code,{children:"public/js/version.js"})," e ",(0,d.jsx)(s.code,{children:"package.json"})," (batem)"]})]})]})]}),"\n",(0,d.jsxs)(s.blockquote,{children:["\n",(0,d.jsxs)(s.p,{children:["Bloco gerado por ",(0,d.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,d.jsx)(s.code,{children:"o comando da coluna direita de cada linha"})]}),"\n"]}),"\n","\n",(0,d.jsxs)(s.p,{children:["E as regras de partida que mais mudam de lugar, todas lidas das constantes de\n",(0,d.jsx)(s.code,{children:"public/js/game.js"}),":"]}),"\n","\n",(0,d.jsxs)(s.table,{children:[(0,d.jsx)(s.thead,{children:(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.th,{children:"Regra"}),(0,d.jsx)(s.th,{children:"Valor"}),(0,d.jsx)(s.th,{children:"Constante"})]})}),(0,d.jsxs)(s.tbody,{children:[(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Fac\xe7\xf5es \xb7 personagens"}),(0,d.jsx)(s.td,{children:"5 \xb7 44 (B 9 \xb7 C 9 \xb7 E 8 \xb7 F 9 \xb7 U 9)"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"CHARACTERS"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Mapas no menu"}),(0,d.jsxs)(s.td,{children:["12 \u2014 2 abrem em rodadas, ",(0,d.jsx)(s.strong,{children:"10 em captura"})]}),(0,d.jsxs)(s.td,{children:[(0,d.jsx)(s.code,{children:"MAPS"})," / ",(0,d.jsx)(s.code,{children:"ctfMode"})]})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Respawn"}),(0,d.jsx)(s.td,{children:"2,2 s"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"RESPAWN_DELAY"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Round"}),(0,d.jsx)(s.td,{children:"99 s, 3 vit\xf3rias"}),(0,d.jsxs)(s.td,{children:[(0,d.jsx)(s.code,{children:"ROUND_TIME"})," / ",(0,d.jsx)(s.code,{children:"ROUNDS_TO_WIN"})]})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Captura"}),(0,d.jsxs)(s.td,{children:["alvo = ",(0,d.jsx)(s.strong,{children:"todas as bandeiras do mapa"}),", 2 rodadas (rede de seguran\xe7a 480 s)"]}),(0,d.jsxs)(s.td,{children:[(0,d.jsx)(s.code,{children:"capsToWin = ctfPts.length"})," / ",(0,d.jsx)(s.code,{children:"CTF_ROUNDS_TO_WIN"})]})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:"Regenera\xe7\xe3o de vida"}),(0,d.jsx)(s.td,{children:(0,d.jsxs)(s.strong,{children:["DESLIGADA \u2014 ",(0,d.jsx)(s.code,{children:"?regen=1"})," religa"]})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"REGEN"})})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsxs)(s.td,{children:["Ranking / p\xe1ginas ",(0,d.jsx)(s.code,{children:"/u/"})]}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"DESLIGADOS \u2014 \xe9 uma flag, volta numa linha"})}),(0,d.jsxs)(s.td,{children:[(0,d.jsx)(s.code,{children:"RANKING_ON"})," em ",(0,d.jsx)(s.code,{children:"src/lib/site.ts"})]})]})]})]}),"\n",(0,d.jsxs)(s.blockquote,{children:["\n",(0,d.jsxs)(s.p,{children:["Bloco gerado por ",(0,d.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,d.jsx)(s.code,{children:"constantes de public/js/game.js \xb7 RANKING_ON de src/lib/site.ts"})]}),"\n"]}),"\n","\n",(0,d.jsxs)(s.p,{children:["O menu aceita de ",(0,d.jsx)(s.strong,{children:"2\xd72 a 8\xd78"})," bots (o motor aceita de 1 a 8 por lado); o padr\xe3o \xe9 4\xd74."]}),"\n",(0,d.jsxs)(s.admonition,{title:"Dois desses s\xe3o escolha recente, n\xe3o defeito",type:"note",children:[(0,d.jsxs)(s.p,{children:[(0,d.jsx)(s.strong,{children:"A regenera\xe7\xe3o de vida foi desligada"})," em 05/08 (",(0,d.jsx)(s.code,{children:"REGEN = QS.get('regen') === '1'"}),"). Ela\nexistia, estilo CoD \u2014 6 s sem tomar dano e 22 HP/s \u2014, e o dono a reportou como bug\n(",(0,d.jsx)(s.em,{children:'"a vida do 1st player volta a 100, n\xe3o sei porque"'}),") justamente porque era ",(0,d.jsx)(s.strong,{children:"invis\xedvel"}),":\nsem \xedcone, sem som, sem linha nas configura\xe7\xf5es. Regra que o jogador n\xe3o percebe \xe9\nindistingu\xedvel de defeito. Ela continua inteira atr\xe1s de ",(0,d.jsx)(s.code,{children:"?regen=1"}),", com a simetria\njogador\u2194bot. ",(0,d.jsx)(s.strong,{children:"Quem religar tem que entregar o feedback junto"})," \u2014 e resolver o que ela\nvinha tapando: sem cura, kit ou colete, cada vida depois do primeiro contato j\xe1 estava\nperdida."]}),(0,d.jsxs)(s.p,{children:[(0,d.jsx)(s.strong,{children:"O ranking foi desligado"})," e trocado por telemetria an\xf4nima. ",(0,d.jsx)(s.code,{children:"/ranking"})," e ",(0,d.jsx)(s.code,{children:"/u/*"}),"\nrespondem ",(0,d.jsxs)(s.strong,{children:["200 com aviso + ",(0,d.jsx)(s.code,{children:"noindex"})]})," (n\xe3o 404 \u2014 as URLs est\xe3o indexadas e v\xe3o voltar),\ne ",(0,d.jsx)(s.code,{children:"/api/leaderboard"})," responde ",(0,d.jsx)(s.code,{children:"{disabled:true}"}),"."]})]}),"\n",(0,d.jsxs)(s.admonition,{title:"O quality gate N\xc3O est\xe1 verde, e isso \xe9 declarado",type:"caution",children:[(0,d.jsxs)(s.p,{children:["Quantas invariantes passam ",(0,d.jsx)(s.strong,{children:"n\xe3o \xe9 deriv\xe1vel do c\xf3digo"})," \u2014 \xe9 o resultado de uma execu\xe7\xe3o,\ne depende at\xe9 de qual insumo existe na m\xe1quina. Por isso esse placar n\xe3o \xe9 repetido aqui:\nele mora no cabe\xe7alho de\n",(0,d.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/KNOWN-BUGS.md",children:(0,d.jsx)(s.code,{children:"KNOWN-BUGS.md"})}),", colado\nde uma execu\xe7\xe3o real, com a lista das vermelhas, causa raiz e ",(0,d.jsx)(s.code,{children:"arquivo:linha"})," de cada uma.\n\xc9 esse arquivo que \xe9 mantido dia a dia."]}),(0,d.jsx)(s.p,{children:"Para o estado de hoje, rode \u2014 n\xe3o repita n\xfamero de cabe\xe7a:"}),(0,d.jsx)(s.pre,{children:(0,d.jsx)(s.code,{className:"language-bash",children:"npm run eval:vm && node tools/eval/invariants.mjs --json # 10-12 min\n"})}),(0,d.jsxs)(s.p,{children:[(0,d.jsx)(s.strong,{children:"A ordem importa"}),": invariante de viewmodel medida com o JSON de ontem inventa vermelha\n(ver ",(0,d.jsx)(s.a,{href:"/docs/colaborar#rodar-o-quality-gate",children:"Como colaborar"}),")."]})]}),"\n",(0,d.jsx)(s.h2,{id:"rodar-em-3-comandos",children:"Rodar em 3 comandos"}),"\n",(0,d.jsx)(s.pre,{children:(0,d.jsx)(s.code,{className:"language-bash",children:"git clone https://github.com/rubenmarcus/csbrasil.git && cd csbrasil\nnpm install\nnpm run dev # abre em http://localhost:4321 \u2014 essa p\xe1gina J\xc1 \xc9 o jogo\n"})}),"\n",(0,d.jsxs)(s.p,{children:["O pacote de \xe1udio (",(0,d.jsx)(s.code,{children:"npm run fetch-audio"}),") \xe9 ",(0,d.jsx)(s.strong,{children:"opcional"}),": sem ele o jogo usa sons\nsintetizados. A pasta ",(0,d.jsx)(s.code,{children:"public/audio/"})," n\xe3o \xe9 versionada."]}),"\n",(0,d.jsx)(s.h3,{id:"linux-webgl-e-modo-compatibilidade",children:"Linux, WebGL e modo compatibilidade"}),"\n",(0,d.jsx)(s.p,{children:"O jogo tenta WebGL2 e WebGL1, come\xe7ando pela escolha padr\xe3o do navegador e reduzindo\nantialias, prefer\xeancia de GPU e stencil antes de desistir. Quando cai em WebGL1,\nllvmpipe/SwiftShader ou outro degrau reduzido, ativa qualidade baixa apenas naquela\nsess\xe3o: DPR 0,75, sem bloom/sombras e com retratos est\xe1ticos na sele\xe7\xe3o."}),"\n",(0,d.jsxs)(s.p,{children:["Use ",(0,d.jsx)(s.code,{children:"?safe=1"})," para priorizar WebGL1 e o caminho de menor custo. Se nem esse modo abrir,\nconfira ",(0,d.jsx)(s.code,{children:"chrome://gpu"})," ou a se\xe7\xe3o Graphics de ",(0,d.jsx)(s.code,{children:"about:support"}),", ligue acelera\xe7\xe3o por\nhardware e atualize Mesa/driver pelo gerenciador da distribui\xe7\xe3o. Uma p\xe1gina n\xe3o pode\nfor\xe7ar um driver quando o navegador recusa criar at\xe9 o contexto WebGL1."]}),"\n",(0,d.jsx)(s.h3,{id:"alternativa-sem-astro-zero-depend\xeancia-de-build",children:"Alternativa sem Astro (zero depend\xeancia de build)"}),"\n",(0,d.jsxs)(s.p,{children:["O arn\xeas de avalia\xe7\xe3o traz um servidor est\xe1tico de 24 linhas que serve ",(0,d.jsx)(s.code,{children:"public/"})," e\nmapeia ",(0,d.jsx)(s.code,{children:"/"})," para o fonte da p\xe1gina do jogo:"]}),"\n",(0,d.jsx)(s.pre,{children:(0,d.jsx)(s.code,{className:"language-bash",children:"node tools/eval/serve.mjs 8123 # http://localhost:8123\n"})}),"\n",(0,d.jsxs)(s.p,{children:["Ele existe exatamente porque ",(0,d.jsx)(s.code,{children:"src/pages/index.astro"})," \xe9 HTML puro \u2014 d\xe1 pra servir o\narquivo cru sem passar pelo Astro (",(0,d.jsx)(s.code,{children:"tools/eval/serve.mjs:15"}),")."]}),"\n",(0,d.jsx)(s.h2,{id:"a-pegadinha-que-custa-a-primeira-hora-de-todo-mundo",children:"A pegadinha que custa a primeira hora de todo mundo"}),"\n",(0,d.jsxs)(s.p,{children:[(0,d.jsxs)(s.strong,{children:["N\xe3o existe ",(0,d.jsx)(s.code,{children:"public/index.html"}),"."]})," Servir a pasta ",(0,d.jsx)(s.code,{children:"public/"})," estaticamente te d\xe1 um\n\xedndice de diret\xf3rio com ",(0,d.jsx)(s.code,{children:"eval.html"}),", ",(0,d.jsx)(s.code,{children:"mapview.html"})," e companhia \u2014 nenhum deles \xe9 o jogo.\nO HTML do jogo \xe9 ",(0,d.jsx)(s.code,{children:"src/pages/index.astro"}),", servido na ",(0,d.jsx)(s.strong,{children:"rota raiz"})," pelo Astro. N\xe3o h\xe1\nrota ",(0,d.jsx)(s.code,{children:"/game"}),"."]}),"\n",(0,d.jsxs)(s.p,{children:["A confirma\xe7\xe3o independente est\xe1 no pr\xf3prio arn\xeas: ",(0,d.jsx)(s.code,{children:"tools/eval/serve.mjs:15"})," precisa de um\ncaso especial ",(0,d.jsx)(s.code,{children:"if (p === '/')"})," que l\xea ",(0,d.jsx)(s.code,{children:"src/pages/index.astro"})," do disco, justamente porque\nn\xe3o h\xe1 ",(0,d.jsx)(s.code,{children:"index.html"})," em ",(0,d.jsx)(s.code,{children:"public/"})," pra servir."]}),"\n",(0,d.jsx)(s.admonition,{title:"Esta se\xe7\xe3o j\xe1 foi uma lista de erros do README",type:"note",children:(0,d.jsxs)(s.p,{children:["At\xe9 04/08/2026 ela existia porque o ",(0,d.jsx)(s.code,{children:"README.md"})," da raiz mandava rodar\n",(0,d.jsx)(s.code,{children:"cd public && python3 -m http.server"}),' e falava num "jogo em ',(0,d.jsx)(s.code,{children:"/game/"}),'". As duas linhas\nforam corrigidas \u2014 o README hoje diz o certo. O que sobrou \xe9 o fato em si, que continua\nsendo a primeira pedra no caminho de quem chega.']})}),"\n",(0,d.jsx)(s.h2,{id:"estrutura-real-do-reposit\xf3rio",children:"Estrutura real do reposit\xf3rio"}),"\n",(0,d.jsx)(s.p,{children:"Duas zonas de c\xf3digo e uma terceira zona que \xe9 a raz\xe3o desta doc existir (o arn\xeas):"}),"\n",(0,d.jsxs)(s.p,{children:["Nenhuma contagem aqui: a \xe1rvore diz ",(0,d.jsx)(s.strong,{children:"o que \xe9 cada coisa"}),", e os n\xfameros vivem na tabela\ngerada l\xe1 em cima. Misturar os dois \xe9 como o ",(0,d.jsx)(s.code,{children:"ARCH.md"})," escrito \xe0 m\xe3o nasceu errado."]}),"\n",(0,d.jsx)(s.pre,{children:(0,d.jsx)(s.code,{children:'public/ O JOGO \u2014 vanilla ES modules, ZERO build\n js/\n game.js a classe Game (loop, bots, tiro, HUD) \u2014 o maior arquivo do repo\n main.js menu, wiring de DOM, persist\xeancia\n vmattach.js springs.js weapons.js fparms.js handik.js viewmodel/armas\n maps.js o REGISTRO de mapas (quem n\xe3o est\xe1 aqui n\xe3o \xe9 jog\xe1vel)\n map_brasilia.js map_piscina.js map_havan.js\n map_ferrovelho.js map_quebrada.js os mapas registrados\n map_piscinao_ramos.js "Piscin\xe3o" \u2014 existe no disco, FORA do registro\n mapprops.js map_decals.js props e grafite\n bloom.js textures.js vao.js stylize.js gpuparticles.js gr\xe1ficos/FX\n characters.js glbchars.js personagens\n audio.js version.js site-bg.js\n models/ armas, personagens, props e clipes de anima\xe7\xe3o em GLB\n vendor/ Three.js vendorizado (sem CDN, sem npm no runtime)\n style.css o HUD inteiro\n *.html arn\xeases visuais (eval, mapview, weapontest, vm-inspect\u2026)\n\nsrc/ O SITE (Astro + adapter Vercel)\n pages/index.astro \u26a0 ISTO \xc9 O JOGO (HTML + import map + HUD)\n pages/sobre.astro landing/FAQ com JSON-LD\n pages/personagens.astro como-jogar.astro ranking.astro mapa.astro\n pages/u/[...path].astro perfil p\xfablico\n pages/api/*.ts SSR: leaderboard, submit-match, register, badge, avatar\n layouts/Layout.astro shell do site (n\xe3o do jogo)\n lib/ supabase, svg, geo, fmt\n\ntools/\n eval/ O ARN\xcaS \u2014 r\xe9guas, quality gate e sondas. Ver "Quality gates"\n invariants.mjs o quality gate\n ref-measure.py mede os frames de refer\xeancia (a doutrina da casa)\n harness.mjs sobe o Game real em node com DOM stubado\n ARCH.md BAR.md mapa de conflito (gerado) e a r\xe9gua visual\n gen-arch.mjs gera e VALIDA o ARCH.md\n gen-docs.mjs gera e VALIDA os blocos num\xe9ricos desta documenta\xe7\xe3o\n gen-asset.mjs gera prop 3D por texto (Tripo/Meshy)\n gen-image.mjs gera arte 2D por texto (OpenRouter)\n\n (banco: schema/migrations s\xe3o PRIVADOS \u2014 fora do repo)\n.github/workflows/ci.yml o quality gate rodando em CI\n'})}),"\n",(0,d.jsx)(s.p,{children:"Os mapas registrados hoje, e em que modo cada um abre:"}),"\n","\n",(0,d.jsxs)(s.table,{children:[(0,d.jsx)(s.thead,{children:(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.th,{children:"Id"}),(0,d.jsx)(s.th,{children:"Nome no menu"}),(0,d.jsx)(s.th,{children:"Abre em"}),(0,d.jsxs)(s.th,{children:["Arquivo em ",(0,d.jsx)(s.code,{children:"public/js/"})]}),(0,d.jsx)(s.th,{style:{textAlign:"right"},children:"Linhas"})]})}),(0,d.jsxs)(s.tbody,{children:[(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"praca_poderes"})}),(0,d.jsx)(s.td,{children:"Pra\xe7a dos Tr\xeas Poderes"}),(0,d.jsx)(s.td,{children:"rodadas"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_brasilia.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"1.830"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"piscina_treta"})}),(0,d.jsx)(s.td,{children:"Piscina da Treta"}),(0,d.jsx)(s.td,{children:"rodadas"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_piscina.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"810"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"loja_h"})}),(0,d.jsx)(s.td,{children:"Loja H (Estacionamento)"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_havan.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"1.964"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"ferro_velho"})}),(0,d.jsx)(s.td,{children:"Ferro Velho do Z\xe9"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_ferrovelho.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"1.888"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"quebrada"})}),(0,d.jsx)(s.td,{children:"Quebrada (Rua do Baile)"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_quebrada.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"1.599"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"posto_treta"})}),(0,d.jsx)(s.td,{children:"Posto da Treta"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_posto.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"489"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"upa_24h"})}),(0,d.jsx)(s.td,{children:"UPA 24h da Treta"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_upa.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"288"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"obras_prefeitura"})}),(0,d.jsx)(s.td,{children:"Obras da Prefeitura"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_obras.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"240"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"atacadao_treta"})}),(0,d.jsx)(s.td,{children:"Atacad\xe3o da Treta"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_atacadao.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"255"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"parque_treta"})}),(0,d.jsx)(s.td,{children:"Parque da Treta"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_parque.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"402"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"velho_oeste"})}),(0,d.jsx)(s.td,{children:"Velho Oeste da Treta"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_velho_oeste.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"433"})]}),(0,d.jsxs)(s.tr,{children:[(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"penitenciaria"})}),(0,d.jsx)(s.td,{children:"Penitenci\xe1ria da Treta"}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.strong,{children:"captura"})}),(0,d.jsx)(s.td,{children:(0,d.jsx)(s.code,{children:"map_penitenciaria.js"})}),(0,d.jsx)(s.td,{style:{textAlign:"right"},children:"247"})]})]})]}),"\n",(0,d.jsxs)(s.p,{children:[(0,d.jsx)(s.strong,{children:"12 mapas registrados"})," \u2014 2 abrem em rodadas e 10 em captura. ",(0,d.jsx)(s.code,{children:"ctfMode"})," ",(0,d.jsx)(s.strong,{children:"abre"})," o mapa em captura, n\xe3o prende: o jogador troca no menu (\xe9 a ",(0,d.jsx)(s.code,{children:"MOD1"}),"). H\xe1 14 arquivos ",(0,d.jsx)(s.code,{children:"map_*.js"})," em ",(0,d.jsx)(s.code,{children:"public/js/"})," \u2014 arquivo no disco ",(0,d.jsx)(s.strong,{children:"n\xe3o"})," implica mapa jog\xe1vel."]}),"\n",(0,d.jsxs)(s.blockquote,{children:["\n",(0,d.jsxs)(s.p,{children:["Bloco gerado por ",(0,d.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,d.jsx)(s.code,{children:"objeto MAPS de public/js/maps.js"})]}),"\n"]}),"\n","\n",(0,d.jsx)(s.h3,{id:"as-duas-zonas",children:"As duas zonas"}),"\n",(0,d.jsxs)(s.p,{children:["Em uma linha cada: ",(0,d.jsxs)(s.strong,{children:[(0,d.jsx)(s.code,{children:"public/"})," \xe9 o jogo"]})," (vanilla, ES modules, sem framework e sem\nbundler) e ",(0,d.jsxs)(s.strong,{children:[(0,d.jsx)(s.code,{children:"src/"})," \xe9 o site"]})," (Astro com SSR, onde framework \xe9 bem-vindo). O que cada\nregra da fronteira paga, e por que ela \xe9 dura, est\xe1 em\n",(0,d.jsx)(s.strong,{children:(0,d.jsx)(s.a,{href:"/docs/stack#as-duas-zonas-e-por-que-a-fronteira-%C3%A9-dura",children:"Stack e ferramentas"})})," \u2014 uma\np\xe1gina s\xf3, para n\xe3o haver duas vers\xf5es da mesma fronteira."]}),"\n",(0,d.jsxs)(s.p,{children:["O que voc\xea precisa saber ",(0,d.jsx)(s.strong,{children:"antes de editar"})," \xe9 a consequ\xeancia: o jogo \xe9 carregado pela\np\xe1gina Astro via ",(0,d.jsx)(s.strong,{children:"import map com vers\xe3o e hash do conte\xfado"})," (",(0,d.jsx)(s.code,{children:"src/pages/index.astro"}),")."]}),"\n",(0,d.jsx)(s.admonition,{title:"Preserve o manifesto publicado",type:"danger",children:(0,d.jsxs)(s.p,{children:[(0,d.jsx)(s.code,{children:"scripts/module-cache.mjs"})," deriva o hash dos m\xf3dulos publicados sob ",(0,d.jsx)(s.code,{children:"public/js/"})," e o import map\naplica essa revis\xe3o ao grafo inteiro. N\xe3o fa\xe7a bump manual e n\xe3o inclua bancadas que\n",(0,d.jsx)(s.code,{children:"scripts/prune-dist.mjs"})," remove. ",(0,d.jsx)(s.code,{children:"npm run eval:shaderbudget"})," (SB7) confere as duas propriedades."]})}),"\n",(0,d.jsx)(s.h2,{id:"comandos-que-voc\xea-vai-usar",children:"Comandos que voc\xea vai usar"}),"\n",(0,d.jsx)(s.pre,{children:(0,d.jsx)(s.code,{className:"language-bash",children:"npm run dev # site + jogo (Astro, :4321) \u2014 a rota / J\xc1 \xc9 o jogo\nnpm run build # dist/client + dist/server\nnpm run eval:vm # enquadramento do viewmodel \u2014 RODE ANTES das invariantes\nnpm run eval:invariants # as invariantes \u2014 node puro, 10-12 min\nnpm run eval:bots # botsim 60 s por mapa, sementes fixas\nnpm run eval:mat # material/luz/fog/textura nos mapas\nnpm run docs # regenera os blocos num\xe9ricos desta documenta\xe7\xe3o\nnode tools/eval/serve.mjs 8123 # servidor est\xe1tico sem Astro\n"})}),"\n",(0,d.jsxs)(s.p,{children:["E os dois quality gates, com a lista exata do que cada um roda \u2014 direto do ",(0,d.jsx)(s.code,{children:"package.json"}),":"]}),"\n","\n",(0,d.jsx)(s.pre,{children:(0,d.jsx)(s.code,{className:"language-bash",children:"npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam\n"})}),"\n",(0,d.jsxs)(s.p,{children:[(0,d.jsx)(s.code,{children:"package.json"})," tem ",(0,d.jsx)(s.strong,{children:"124 scripts"}),"; o motivo de cada um mora em ",(0,d.jsx)(s.code,{children:"SCRIPTS.md"})," (migrado das chaves ",(0,d.jsx)(s.code,{children:"//nome"})," em 18/08/2026) \u2014 \xe9 onde est\xe1 o porqu\xea."]}),"\n",(0,d.jsxs)(s.blockquote,{children:["\n",(0,d.jsxs)(s.p,{children:["Bloco gerado por ",(0,d.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,d.jsx)(s.code,{children:"node -p \"Object.keys(require('./package.json').scripts)\""})]}),"\n"]}),"\n","\n",(0,d.jsxs)(s.p,{children:["O ",(0,d.jsx)(s.code,{children:"check:fast"})," cobre as r\xe9guas de node puro; o CI (",(0,d.jsx)(s.code,{children:".github/workflows/ci.yml"}),") roda o\nmesmo conjunto mais os passos que exigem rede."]}),"\n",(0,d.jsxs)(s.admonition,{type:"tip",children:[(0,d.jsxs)(s.mdxAdmonitionTitle,{children:["Use o ",(0,d.jsx)(s.code,{children:"check:fast"})," no loop, o ",(0,d.jsx)(s.code,{children:"portao-browser"})," antes do PR"]}),(0,d.jsxs)(s.p,{children:["O ",(0,d.jsx)(s.code,{children:"portao-browser"})," gasta 10-12 min porque sobe o jogo cinco vezes. O ",(0,d.jsx)(s.code,{children:"check:fast"})," cobre as r\xe9guas\nque nasceram dos bugs mais recentes (menu de pausa, rodada de captura, regenera\xe7\xe3o,\nmanifesto de anima\xe7\xe3o) e roda em cerca de um minuto."]})]}),"\n",(0,d.jsx)(s.h2,{id:"onde-ir-agora",children:"Onde ir agora"}),"\n",(0,d.jsxs)(s.p,{children:["A ordem da barra lateral ",(0,d.jsx)(s.strong,{children:"\xe9"})," a ordem de leitura, e cada p\xe1gina entrega uma coisa:"]}),"\n",(0,d.jsxs)(s.ol,{children:["\n",(0,d.jsxs)(s.li,{children:[(0,d.jsx)(s.strong,{children:(0,d.jsx)(s.a,{href:"/docs/stack",children:"Stack e ferramentas"})})," \u2014 com o que isso \xe9 feito, com a vers\xe3o declarada\nde cada pe\xe7a. \xc9 onde a fronteira ",(0,d.jsx)(s.code,{children:"public/"})," \xd7 ",(0,d.jsx)(s.code,{children:"src/"})," est\xe1 explicada por inteiro."]}),"\n",(0,d.jsxs)(s.li,{children:[(0,d.jsx)(s.strong,{children:(0,d.jsx)(s.a,{href:"/docs/instrumentacao-ai",children:"Instrumenta\xe7\xe3o de IA"})})," \u2014 como o trabalho \xe9 feito aqui. Se\nvoc\xea nunca colaborou com agentes num reposit\xf3rio, comece por essa."]}),"\n",(0,d.jsxs)(s.li,{children:[(0,d.jsx)(s.strong,{children:(0,d.jsx)(s.a,{href:"/docs/quality-gates",children:"O quality gate"})})," \u2014 o que \xe9 uma invariante, como se escreve uma, as\nduas leis da casa e o teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua. ",(0,d.jsx)(s.strong,{children:"\xc9 a p\xe1gina mais \xfatil do\nsite."})]}),"\n",(0,d.jsxs)(s.li,{children:[(0,d.jsx)(s.strong,{children:(0,d.jsx)(s.a,{href:"/docs/arquitetura",children:"Arquitetura"})})," \u2014 como N agentes editam o mesmo arquivo sem\ncolidir, e a tabela de conflito. Leia antes de tocar em ",(0,d.jsx)(s.code,{children:"game.js"}),"."]}),"\n",(0,d.jsxs)(s.li,{children:[(0,d.jsx)(s.strong,{children:(0,d.jsx)(s.a,{href:"/docs/colaborar",children:"Como colaborar"})})," \u2014 o que um PR precisa pra entrar, e as ",(0,d.jsx)(s.strong,{children:"tarefas\nde primeira contribui\xe7\xe3o"})," j\xe1 escritas em\n",(0,d.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/tree/main/docs/issues",children:(0,d.jsx)(s.code,{children:"docs/issues/"})})," (com um\n",(0,d.jsx)(s.code,{children:"abrir-issues.sh"})," pronto \u2014 elas ainda n\xe3o foram abertas no GitHub)."]}),"\n",(0,d.jsxs)(s.li,{children:[(0,d.jsx)(s.strong,{children:"Licen\xe7a"})," \u2014 o ",(0,d.jsx)(s.code,{children:"LICENSE"})," na raiz declara (hoje AGPL-3.0); as superf\xedcies que\nrepetem o nome e mudam junto est\xe3o no ",(0,d.jsx)(s.code,{children:"CONTRIBUTING.md"}),"."]}),"\n",(0,d.jsxs)(s.li,{children:[(0,d.jsx)(s.strong,{children:(0,d.jsx)(s.a,{href:"/docs/estado",children:"Estado atual"})})," \u2014 fontes vivas de produ\xe7\xe3o, dados e d\xedvida conhecida\ndesde a \xfaltima medi\xe7\xe3o colada."]}),"\n"]}),"\n",(0,d.jsxs)(s.p,{children:["Para onde o projeto ",(0,d.jsx)(s.strong,{children:"vai"})," n\xe3o est\xe1 nesta documenta\xe7\xe3o: \xe9 o\n",(0,d.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,d.jsx)(s.code,{children:"docs/ROADMAP.md"})}),", e o\nplano execut\xe1vel \xe9 o\n",(0,d.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/plans/08-RELEASE-PROFISSIONAL.md",children:(0,d.jsx)(s.code,{children:"plans/08"})}),"."]})]})}function j(e={}){const{wrapper:s}={...(0,o.R)(),...e.components};return s?(0,d.jsx)(s,{...e,children:(0,d.jsx)(h,{...e})}):h(e)}},8453(e,s,r){r.d(s,{R:()=>n,x:()=>i});var a=r(6540);const d={},o=a.createContext(d);function n(e){const s=a.useContext(o);return a.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function i(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:n(e.components),a.createElement(o.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/assets/js/1be81749.cc5d4388.js b/public/docs/assets/js/1be81749.cc5d4388.js deleted file mode 100644 index 130fa3673..000000000 --- a/public/docs/assets/js/1be81749.cc5d4388.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[568],{4208(e,s,r){r.r(s),r.d(s,{assets:()=>t,contentTitle:()=>c,default:()=>j,frontMatter:()=>i,metadata:()=>d,toc:()=>l});const d=JSON.parse('{"id":"comecando","title":"O que \xe9, e como rodar","description":"O que \xe9 o CORO SOLTO, como rodar em 3 comandos e a estrutura real do reposit\xf3rio \u2014 conferida contra o c\xf3digo.","source":"@site/docs/comecando.md","sourceDirName":".","slug":"/","permalink":"/docs/","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/comecando.md","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"id":"comecando","title":"O que \xe9, e como rodar","sidebar_label":"Come\xe7ando","sidebar_position":1,"slug":"/","description":"O que \xe9 o CORO SOLTO, como rodar em 3 comandos e a estrutura real do reposit\xf3rio \u2014 conferida contra o c\xf3digo."},"sidebar":"dev","next":{"title":"Stack e ferramentas","permalink":"/docs/stack"}}');var a=r(4848),n=r(8453),o=r(6025);const i={id:"comecando",title:"O que \xe9, e como rodar",sidebar_label:"Come\xe7ando",sidebar_position:1,slug:"/",description:"O que \xe9 o CORO SOLTO, como rodar em 3 comandos e a estrutura real do reposit\xf3rio \u2014 conferida contra o c\xf3digo."},c="O que \xe9, e como rodar",t={},l=[{value:"Rodar em 3 comandos",id:"rodar-em-3-comandos",level:2},{value:"Linux, WebGL e modo compatibilidade",id:"linux-webgl-e-modo-compatibilidade",level:3},{value:"Alternativa sem Astro (zero depend\xeancia de build)",id:"alternativa-sem-astro-zero-depend\xeancia-de-build",level:3},{value:"A pegadinha que custa a primeira hora de todo mundo",id:"a-pegadinha-que-custa-a-primeira-hora-de-todo-mundo",level:2},{value:"Estrutura real do reposit\xf3rio",id:"estrutura-real-do-reposit\xf3rio",level:2},{value:"As duas zonas",id:"as-duas-zonas",level:3},{value:"Comandos que voc\xea vai usar",id:"comandos-que-voc\xea-vai-usar",level:2},{value:"Onde ir agora",id:"onde-ir-agora",level:2}];function h(e){const s={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,n.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:["\n",(0,a.jsx)("div",{className:"cs-hero",children:(0,a.jsx)("img",{className:"cs-hero__bird",src:(0,o.Ay)("/img/canarinho-header.webp"),alt:"CORO SOLTO: Treta Suprema \u2014 o canarinho, mascote do jogo, girando",width:"604",height:"240"})}),"\n",(0,a.jsx)(s.header,{children:(0,a.jsx)(s.h1,{id:"o-que-\xe9-e-como-rodar",children:"O que \xe9, e como rodar"})}),"\n",(0,a.jsxs)(s.p,{children:[(0,a.jsx)(s.strong,{children:"CORO SOLTO: Treta Suprema"})," (ex-CS BRASIL) \xe9 um FPS de navegador escrito em\nJavaScript vanilla sobre Three.js r160, no estilo do Counter-Strike 1.6: rounds,\nbots, AWP, placar por Tab, r\xe1dio de voz. Roda num link, sem instalar nada."]}),"\n",(0,a.jsxs)(s.p,{children:["Os n\xfameros abaixo ",(0,a.jsx)(s.strong,{children:"n\xe3o s\xe3o escritos \xe0 m\xe3o"}),": eles s\xe3o regerados por\n",(0,a.jsx)(s.code,{children:"node tools/gen-docs.mjs"})," a partir do c\xf3digo, e ",(0,a.jsx)(s.code,{children:"npm run docs:check"})," (dentro do\n",(0,a.jsx)(s.code,{children:"check:fast"}),") reprova o quality gate quando qualquer um deles diverge da \xe1rvore. Antes disso\nesta p\xe1gina envelhecia no primeiro commit \u2014 ver\n",(0,a.jsx)(s.a,{href:"/docs/arquitetura#o-que-%C3%A9-gerado-e-o-que-n%C3%A3o-%C3%A9",children:"o que \xe9 gerado, e o que n\xe3o \xe9"}),"."]}),"\n","\n",(0,a.jsxs)(s.table,{children:[(0,a.jsx)(s.thead,{children:(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.th,{children:"O que"}),(0,a.jsx)(s.th,{style:{textAlign:"right"},children:"Quanto"}),(0,a.jsx)(s.th,{children:"Onde confere"})]})}),(0,a.jsxs)(s.tbody,{children:[(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"C\xf3digo do jogo"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"31.744 linhas em 44 arquivos"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"git ls-files public/js/*.js | xargs wc -l"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"game.js"})}),(0,a.jsxs)(s.td,{style:{textAlign:"right"},children:[(0,a.jsx)(s.strong,{children:"6.838"})," linhas"]}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"wc -l public/js/game.js"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"main.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"2.646 linhas"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"wc -l public/js/main.js"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Armas com GLB"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"26"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"git ls-files 'public/models/weapons/*.glb' | wc -l"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"GLBs de personagem"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"45"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"git ls-files 'public/models/characters/*.glb' | wc -l"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Props em GLB"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"108"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"git ls-files 'public/models/props/*.glb' | wc -l"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Clipes de anima\xe7\xe3o versionados"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"573"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"git ls-files public/models/anims | wc -l"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Personagens jog\xe1veis"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"44, em 5 fac\xe7\xf5es"}),(0,a.jsxs)(s.td,{children:["array ",(0,a.jsx)(s.code,{children:"CHARACTERS"})," de ",(0,a.jsx)(s.code,{children:"characters.js"})]})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Mapas no registro"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"12"}),(0,a.jsxs)(s.td,{children:["objeto ",(0,a.jsx)(s.code,{children:"MAPS"})," de ",(0,a.jsx)(s.code,{children:"maps.js"})]})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Arn\xeases visuais em HTML"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"15"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"git ls-files 'public/*.html' | wc -l"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Scripts do arn\xeas"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"192"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' | wc -l"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Scripts de pipeline"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"54"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"git ls-files 'tools/*.mjs' | wc -l"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Tarefas de entrada escritas"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"26"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"git ls-files 'docs/issues/[0-9]*.md' | wc -l"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Vers\xe3o"}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:(0,a.jsx)(s.code,{children:"2.0.0-alpha.169"})}),(0,a.jsxs)(s.td,{children:[(0,a.jsx)(s.code,{children:"public/js/version.js"})," e ",(0,a.jsx)(s.code,{children:"package.json"})," (batem)"]})]})]})]}),"\n",(0,a.jsxs)(s.blockquote,{children:["\n",(0,a.jsxs)(s.p,{children:["Bloco gerado por ",(0,a.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,a.jsx)(s.code,{children:"o comando da coluna direita de cada linha"})]}),"\n"]}),"\n","\n",(0,a.jsxs)(s.p,{children:["E as regras de partida que mais mudam de lugar, todas lidas das constantes de\n",(0,a.jsx)(s.code,{children:"public/js/game.js"}),":"]}),"\n","\n",(0,a.jsxs)(s.table,{children:[(0,a.jsx)(s.thead,{children:(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.th,{children:"Regra"}),(0,a.jsx)(s.th,{children:"Valor"}),(0,a.jsx)(s.th,{children:"Constante"})]})}),(0,a.jsxs)(s.tbody,{children:[(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Fac\xe7\xf5es \xb7 personagens"}),(0,a.jsx)(s.td,{children:"5 \xb7 44 (B 9 \xb7 C 9 \xb7 E 8 \xb7 F 9 \xb7 U 9)"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"CHARACTERS"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Mapas no menu"}),(0,a.jsxs)(s.td,{children:["12 \u2014 2 abrem em rodadas, ",(0,a.jsx)(s.strong,{children:"10 em captura"})]}),(0,a.jsxs)(s.td,{children:[(0,a.jsx)(s.code,{children:"MAPS"})," / ",(0,a.jsx)(s.code,{children:"ctfMode"})]})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Respawn"}),(0,a.jsx)(s.td,{children:"2,2 s"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"RESPAWN_DELAY"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Round"}),(0,a.jsx)(s.td,{children:"99 s, 3 vit\xf3rias"}),(0,a.jsxs)(s.td,{children:[(0,a.jsx)(s.code,{children:"ROUND_TIME"})," / ",(0,a.jsx)(s.code,{children:"ROUNDS_TO_WIN"})]})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Captura"}),(0,a.jsxs)(s.td,{children:["alvo = ",(0,a.jsx)(s.strong,{children:"todas as bandeiras do mapa"}),", 2 rodadas (rede de seguran\xe7a 480 s)"]}),(0,a.jsxs)(s.td,{children:[(0,a.jsx)(s.code,{children:"capsToWin = ctfPts.length"})," / ",(0,a.jsx)(s.code,{children:"CTF_ROUNDS_TO_WIN"})]})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:"Regenera\xe7\xe3o de vida"}),(0,a.jsx)(s.td,{children:(0,a.jsxs)(s.strong,{children:["DESLIGADA \u2014 ",(0,a.jsx)(s.code,{children:"?regen=1"})," religa"]})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"REGEN"})})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsxs)(s.td,{children:["Ranking / p\xe1ginas ",(0,a.jsx)(s.code,{children:"/u/"})]}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"DESLIGADOS \u2014 \xe9 uma flag, volta numa linha"})}),(0,a.jsxs)(s.td,{children:[(0,a.jsx)(s.code,{children:"RANKING_ON"})," em ",(0,a.jsx)(s.code,{children:"src/lib/site.ts"})]})]})]})]}),"\n",(0,a.jsxs)(s.blockquote,{children:["\n",(0,a.jsxs)(s.p,{children:["Bloco gerado por ",(0,a.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,a.jsx)(s.code,{children:"constantes de public/js/game.js \xb7 RANKING_ON de src/lib/site.ts"})]}),"\n"]}),"\n","\n",(0,a.jsxs)(s.p,{children:["O menu aceita de ",(0,a.jsx)(s.strong,{children:"2\xd72 a 8\xd78"})," bots (o motor aceita de 1 a 8 por lado); o padr\xe3o \xe9 4\xd74."]}),"\n",(0,a.jsxs)(s.admonition,{title:"Dois desses s\xe3o escolha recente, n\xe3o defeito",type:"note",children:[(0,a.jsxs)(s.p,{children:[(0,a.jsx)(s.strong,{children:"A regenera\xe7\xe3o de vida foi desligada"})," em 05/08 (",(0,a.jsx)(s.code,{children:"REGEN = QS.get('regen') === '1'"}),"). Ela\nexistia, estilo CoD \u2014 6 s sem tomar dano e 22 HP/s \u2014, e o dono a reportou como bug\n(",(0,a.jsx)(s.em,{children:'"a vida do 1st player volta a 100, n\xe3o sei porque"'}),") justamente porque era ",(0,a.jsx)(s.strong,{children:"invis\xedvel"}),":\nsem \xedcone, sem som, sem linha nas configura\xe7\xf5es. Regra que o jogador n\xe3o percebe \xe9\nindistingu\xedvel de defeito. Ela continua inteira atr\xe1s de ",(0,a.jsx)(s.code,{children:"?regen=1"}),", com a simetria\njogador\u2194bot. ",(0,a.jsx)(s.strong,{children:"Quem religar tem que entregar o feedback junto"})," \u2014 e resolver o que ela\nvinha tapando: sem cura, kit ou colete, cada vida depois do primeiro contato j\xe1 estava\nperdida."]}),(0,a.jsxs)(s.p,{children:[(0,a.jsx)(s.strong,{children:"O ranking foi desligado"})," e trocado por telemetria an\xf4nima. ",(0,a.jsx)(s.code,{children:"/ranking"})," e ",(0,a.jsx)(s.code,{children:"/u/*"}),"\nrespondem ",(0,a.jsxs)(s.strong,{children:["200 com aviso + ",(0,a.jsx)(s.code,{children:"noindex"})]})," (n\xe3o 404 \u2014 as URLs est\xe3o indexadas e v\xe3o voltar),\ne ",(0,a.jsx)(s.code,{children:"/api/leaderboard"})," responde ",(0,a.jsx)(s.code,{children:"{disabled:true}"}),"."]})]}),"\n",(0,a.jsxs)(s.admonition,{title:"O quality gate N\xc3O est\xe1 verde, e isso \xe9 declarado",type:"caution",children:[(0,a.jsxs)(s.p,{children:["Quantas invariantes passam ",(0,a.jsx)(s.strong,{children:"n\xe3o \xe9 deriv\xe1vel do c\xf3digo"})," \u2014 \xe9 o resultado de uma execu\xe7\xe3o,\ne depende at\xe9 de qual insumo existe na m\xe1quina. Por isso esse placar n\xe3o \xe9 repetido aqui:\nele mora no cabe\xe7alho de\n",(0,a.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/KNOWN-BUGS.md",children:(0,a.jsx)(s.code,{children:"KNOWN-BUGS.md"})}),", colado\nde uma execu\xe7\xe3o real, com a lista das vermelhas, causa raiz e ",(0,a.jsx)(s.code,{children:"arquivo:linha"})," de cada uma.\n\xc9 esse arquivo que \xe9 mantido dia a dia."]}),(0,a.jsx)(s.p,{children:"Para o estado de hoje, rode \u2014 n\xe3o repita n\xfamero de cabe\xe7a:"}),(0,a.jsx)(s.pre,{children:(0,a.jsx)(s.code,{className:"language-bash",children:"npm run eval:vm && node tools/eval/invariants.mjs --json # 10-12 min\n"})}),(0,a.jsxs)(s.p,{children:[(0,a.jsx)(s.strong,{children:"A ordem importa"}),": invariante de viewmodel medida com o JSON de ontem inventa vermelha\n(ver ",(0,a.jsx)(s.a,{href:"/docs/colaborar#rodar-o-quality-gate",children:"Como colaborar"}),")."]})]}),"\n",(0,a.jsx)(s.h2,{id:"rodar-em-3-comandos",children:"Rodar em 3 comandos"}),"\n",(0,a.jsx)(s.pre,{children:(0,a.jsx)(s.code,{className:"language-bash",children:"git clone https://github.com/rubenmarcus/csbrasil.git && cd csbrasil\nnpm install\nnpm run dev # abre em http://localhost:4321 \u2014 essa p\xe1gina J\xc1 \xc9 o jogo\n"})}),"\n",(0,a.jsxs)(s.p,{children:["O pacote de \xe1udio (",(0,a.jsx)(s.code,{children:"npm run fetch-audio"}),") \xe9 ",(0,a.jsx)(s.strong,{children:"opcional"}),": sem ele o jogo usa sons\nsintetizados. A pasta ",(0,a.jsx)(s.code,{children:"public/audio/"})," n\xe3o \xe9 versionada."]}),"\n",(0,a.jsx)(s.h3,{id:"linux-webgl-e-modo-compatibilidade",children:"Linux, WebGL e modo compatibilidade"}),"\n",(0,a.jsx)(s.p,{children:"O jogo tenta WebGL2 e WebGL1, come\xe7ando pela escolha padr\xe3o do navegador e reduzindo\nantialias, prefer\xeancia de GPU e stencil antes de desistir. Quando cai em WebGL1,\nllvmpipe/SwiftShader ou outro degrau reduzido, ativa qualidade baixa apenas naquela\nsess\xe3o: DPR 0,75, sem bloom/sombras e com retratos est\xe1ticos na sele\xe7\xe3o."}),"\n",(0,a.jsxs)(s.p,{children:["Use ",(0,a.jsx)(s.code,{children:"?safe=1"})," para priorizar WebGL1 e o caminho de menor custo. Se nem esse modo abrir,\nconfira ",(0,a.jsx)(s.code,{children:"chrome://gpu"})," ou a se\xe7\xe3o Graphics de ",(0,a.jsx)(s.code,{children:"about:support"}),", ligue acelera\xe7\xe3o por\nhardware e atualize Mesa/driver pelo gerenciador da distribui\xe7\xe3o. Uma p\xe1gina n\xe3o pode\nfor\xe7ar um driver quando o navegador recusa criar at\xe9 o contexto WebGL1."]}),"\n",(0,a.jsx)(s.h3,{id:"alternativa-sem-astro-zero-depend\xeancia-de-build",children:"Alternativa sem Astro (zero depend\xeancia de build)"}),"\n",(0,a.jsxs)(s.p,{children:["O arn\xeas de avalia\xe7\xe3o traz um servidor est\xe1tico de 24 linhas que serve ",(0,a.jsx)(s.code,{children:"public/"})," e\nmapeia ",(0,a.jsx)(s.code,{children:"/"})," para o fonte da p\xe1gina do jogo:"]}),"\n",(0,a.jsx)(s.pre,{children:(0,a.jsx)(s.code,{className:"language-bash",children:"node tools/eval/serve.mjs 8123 # http://localhost:8123\n"})}),"\n",(0,a.jsxs)(s.p,{children:["Ele existe exatamente porque ",(0,a.jsx)(s.code,{children:"src/pages/index.astro"})," \xe9 HTML puro \u2014 d\xe1 pra servir o\narquivo cru sem passar pelo Astro (",(0,a.jsx)(s.code,{children:"tools/eval/serve.mjs:15"}),")."]}),"\n",(0,a.jsx)(s.h2,{id:"a-pegadinha-que-custa-a-primeira-hora-de-todo-mundo",children:"A pegadinha que custa a primeira hora de todo mundo"}),"\n",(0,a.jsxs)(s.p,{children:[(0,a.jsxs)(s.strong,{children:["N\xe3o existe ",(0,a.jsx)(s.code,{children:"public/index.html"}),"."]})," Servir a pasta ",(0,a.jsx)(s.code,{children:"public/"})," estaticamente te d\xe1 um\n\xedndice de diret\xf3rio com ",(0,a.jsx)(s.code,{children:"eval.html"}),", ",(0,a.jsx)(s.code,{children:"mapview.html"})," e companhia \u2014 nenhum deles \xe9 o jogo.\nO HTML do jogo \xe9 ",(0,a.jsx)(s.code,{children:"src/pages/index.astro"}),", servido na ",(0,a.jsx)(s.strong,{children:"rota raiz"})," pelo Astro. N\xe3o h\xe1\nrota ",(0,a.jsx)(s.code,{children:"/game"}),"."]}),"\n",(0,a.jsxs)(s.p,{children:["A confirma\xe7\xe3o independente est\xe1 no pr\xf3prio arn\xeas: ",(0,a.jsx)(s.code,{children:"tools/eval/serve.mjs:15"})," precisa de um\ncaso especial ",(0,a.jsx)(s.code,{children:"if (p === '/')"})," que l\xea ",(0,a.jsx)(s.code,{children:"src/pages/index.astro"})," do disco, justamente porque\nn\xe3o h\xe1 ",(0,a.jsx)(s.code,{children:"index.html"})," em ",(0,a.jsx)(s.code,{children:"public/"})," pra servir."]}),"\n",(0,a.jsx)(s.admonition,{title:"Esta se\xe7\xe3o j\xe1 foi uma lista de erros do README",type:"note",children:(0,a.jsxs)(s.p,{children:["At\xe9 04/08/2026 ela existia porque o ",(0,a.jsx)(s.code,{children:"README.md"})," da raiz mandava rodar\n",(0,a.jsx)(s.code,{children:"cd public && python3 -m http.server"}),' e falava num "jogo em ',(0,a.jsx)(s.code,{children:"/game/"}),'". As duas linhas\nforam corrigidas \u2014 o README hoje diz o certo. O que sobrou \xe9 o fato em si, que continua\nsendo a primeira pedra no caminho de quem chega.']})}),"\n",(0,a.jsx)(s.h2,{id:"estrutura-real-do-reposit\xf3rio",children:"Estrutura real do reposit\xf3rio"}),"\n",(0,a.jsx)(s.p,{children:"Duas zonas de c\xf3digo e uma terceira zona que \xe9 a raz\xe3o desta doc existir (o arn\xeas):"}),"\n",(0,a.jsxs)(s.p,{children:["Nenhuma contagem aqui: a \xe1rvore diz ",(0,a.jsx)(s.strong,{children:"o que \xe9 cada coisa"}),", e os n\xfameros vivem na tabela\ngerada l\xe1 em cima. Misturar os dois \xe9 como o ",(0,a.jsx)(s.code,{children:"ARCH.md"})," escrito \xe0 m\xe3o nasceu errado."]}),"\n",(0,a.jsx)(s.pre,{children:(0,a.jsx)(s.code,{children:'public/ O JOGO \u2014 vanilla ES modules, ZERO build\n js/\n game.js a classe Game (loop, bots, tiro, HUD) \u2014 o maior arquivo do repo\n main.js menu, wiring de DOM, persist\xeancia\n vmattach.js springs.js weapons.js fparms.js handik.js viewmodel/armas\n maps.js o REGISTRO de mapas (quem n\xe3o est\xe1 aqui n\xe3o \xe9 jog\xe1vel)\n map_brasilia.js map_piscina.js map_havan.js\n map_ferrovelho.js map_quebrada.js os mapas registrados\n map_piscinao_ramos.js "Piscin\xe3o" \u2014 existe no disco, FORA do registro\n mapprops.js map_decals.js props e grafite\n bloom.js textures.js vao.js stylize.js gpuparticles.js gr\xe1ficos/FX\n characters.js glbchars.js personagens\n audio.js version.js site-bg.js\n models/ armas, personagens, props e clipes de anima\xe7\xe3o em GLB\n vendor/ Three.js vendorizado (sem CDN, sem npm no runtime)\n style.css o HUD inteiro\n *.html arn\xeases visuais (eval, mapview, weapontest, vm-inspect\u2026)\n\nsrc/ O SITE (Astro + adapter Vercel)\n pages/index.astro \u26a0 ISTO \xc9 O JOGO (HTML + import map + HUD)\n pages/sobre.astro landing/FAQ com JSON-LD\n pages/personagens.astro como-jogar.astro ranking.astro mapa.astro\n pages/u/[...path].astro perfil p\xfablico\n pages/api/*.ts SSR: leaderboard, submit-match, register, badge, avatar\n layouts/Layout.astro shell do site (n\xe3o do jogo)\n lib/ supabase, svg, geo, fmt\n\ntools/\n eval/ O ARN\xcaS \u2014 r\xe9guas, quality gate e sondas. Ver "Quality gates"\n invariants.mjs o quality gate\n ref-measure.py mede os frames de refer\xeancia (a doutrina da casa)\n harness.mjs sobe o Game real em node com DOM stubado\n ARCH.md BAR.md mapa de conflito (gerado) e a r\xe9gua visual\n gen-arch.mjs gera e VALIDA o ARCH.md\n gen-docs.mjs gera e VALIDA os blocos num\xe9ricos desta documenta\xe7\xe3o\n gen-asset.mjs gera prop 3D por texto (Tripo/Meshy)\n gen-image.mjs gera arte 2D por texto (OpenRouter)\n\n (banco: schema/migrations s\xe3o PRIVADOS \u2014 fora do repo)\n.github/workflows/ci.yml o quality gate rodando em CI\n'})}),"\n",(0,a.jsx)(s.p,{children:"Os mapas registrados hoje, e em que modo cada um abre:"}),"\n","\n",(0,a.jsxs)(s.table,{children:[(0,a.jsx)(s.thead,{children:(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.th,{children:"Id"}),(0,a.jsx)(s.th,{children:"Nome no menu"}),(0,a.jsx)(s.th,{children:"Abre em"}),(0,a.jsxs)(s.th,{children:["Arquivo em ",(0,a.jsx)(s.code,{children:"public/js/"})]}),(0,a.jsx)(s.th,{style:{textAlign:"right"},children:"Linhas"})]})}),(0,a.jsxs)(s.tbody,{children:[(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"praca_poderes"})}),(0,a.jsx)(s.td,{children:"Pra\xe7a dos Tr\xeas Poderes"}),(0,a.jsx)(s.td,{children:"rodadas"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_brasilia.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"1.830"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"piscina_treta"})}),(0,a.jsx)(s.td,{children:"Piscina da Treta"}),(0,a.jsx)(s.td,{children:"rodadas"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_piscina.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"810"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"loja_h"})}),(0,a.jsx)(s.td,{children:"Loja H (Estacionamento)"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_havan.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"1.964"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"ferro_velho"})}),(0,a.jsx)(s.td,{children:"Ferro Velho do Z\xe9"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_ferrovelho.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"1.888"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"quebrada"})}),(0,a.jsx)(s.td,{children:"Quebrada (Rua do Baile)"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_quebrada.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"1.599"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"posto_treta"})}),(0,a.jsx)(s.td,{children:"Posto da Treta"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_posto.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"489"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"upa_24h"})}),(0,a.jsx)(s.td,{children:"UPA 24h da Treta"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_upa.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"288"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"obras_prefeitura"})}),(0,a.jsx)(s.td,{children:"Obras da Prefeitura"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_obras.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"240"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"atacadao_treta"})}),(0,a.jsx)(s.td,{children:"Atacad\xe3o da Treta"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_atacadao.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"255"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"parque_treta"})}),(0,a.jsx)(s.td,{children:"Parque da Treta"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_parque.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"402"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"velho_oeste"})}),(0,a.jsx)(s.td,{children:"Velho Oeste da Treta"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_velho_oeste.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"433"})]}),(0,a.jsxs)(s.tr,{children:[(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"penitenciaria"})}),(0,a.jsx)(s.td,{children:"Penitenci\xe1ria da Treta"}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.strong,{children:"captura"})}),(0,a.jsx)(s.td,{children:(0,a.jsx)(s.code,{children:"map_penitenciaria.js"})}),(0,a.jsx)(s.td,{style:{textAlign:"right"},children:"247"})]})]})]}),"\n",(0,a.jsxs)(s.p,{children:[(0,a.jsx)(s.strong,{children:"12 mapas registrados"})," \u2014 2 abrem em rodadas e 10 em captura. ",(0,a.jsx)(s.code,{children:"ctfMode"})," ",(0,a.jsx)(s.strong,{children:"abre"})," o mapa em captura, n\xe3o prende: o jogador troca no menu (\xe9 a ",(0,a.jsx)(s.code,{children:"MOD1"}),"). H\xe1 14 arquivos ",(0,a.jsx)(s.code,{children:"map_*.js"})," em ",(0,a.jsx)(s.code,{children:"public/js/"})," \u2014 arquivo no disco ",(0,a.jsx)(s.strong,{children:"n\xe3o"})," implica mapa jog\xe1vel."]}),"\n",(0,a.jsxs)(s.blockquote,{children:["\n",(0,a.jsxs)(s.p,{children:["Bloco gerado por ",(0,a.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,a.jsx)(s.code,{children:"objeto MAPS de public/js/maps.js"})]}),"\n"]}),"\n","\n",(0,a.jsx)(s.h3,{id:"as-duas-zonas",children:"As duas zonas"}),"\n",(0,a.jsxs)(s.p,{children:["Em uma linha cada: ",(0,a.jsxs)(s.strong,{children:[(0,a.jsx)(s.code,{children:"public/"})," \xe9 o jogo"]})," (vanilla, ES modules, sem framework e sem\nbundler) e ",(0,a.jsxs)(s.strong,{children:[(0,a.jsx)(s.code,{children:"src/"})," \xe9 o site"]})," (Astro com SSR, onde framework \xe9 bem-vindo). O que cada\nregra da fronteira paga, e por que ela \xe9 dura, est\xe1 em\n",(0,a.jsx)(s.strong,{children:(0,a.jsx)(s.a,{href:"/docs/stack#as-duas-zonas-e-por-que-a-fronteira-%C3%A9-dura",children:"Stack e ferramentas"})})," \u2014 uma\np\xe1gina s\xf3, para n\xe3o haver duas vers\xf5es da mesma fronteira."]}),"\n",(0,a.jsxs)(s.p,{children:["O que voc\xea precisa saber ",(0,a.jsx)(s.strong,{children:"antes de editar"})," \xe9 a consequ\xeancia: o jogo \xe9 carregado pela\np\xe1gina Astro via ",(0,a.jsx)(s.strong,{children:"import map com vers\xe3o e hash do conte\xfado"})," (",(0,a.jsx)(s.code,{children:"src/pages/index.astro"}),")."]}),"\n",(0,a.jsx)(s.admonition,{title:"Preserve o manifesto publicado",type:"danger",children:(0,a.jsxs)(s.p,{children:[(0,a.jsx)(s.code,{children:"scripts/module-cache.mjs"})," deriva o hash dos m\xf3dulos publicados sob ",(0,a.jsx)(s.code,{children:"public/js/"})," e o import map\naplica essa revis\xe3o ao grafo inteiro. N\xe3o fa\xe7a bump manual e n\xe3o inclua bancadas que\n",(0,a.jsx)(s.code,{children:"scripts/prune-dist.mjs"})," remove. ",(0,a.jsx)(s.code,{children:"npm run eval:shaderbudget"})," (SB7) confere as duas propriedades."]})}),"\n",(0,a.jsx)(s.h2,{id:"comandos-que-voc\xea-vai-usar",children:"Comandos que voc\xea vai usar"}),"\n",(0,a.jsx)(s.pre,{children:(0,a.jsx)(s.code,{className:"language-bash",children:"npm run dev # site + jogo (Astro, :4321) \u2014 a rota / J\xc1 \xc9 o jogo\nnpm run build # dist/client + dist/server\nnpm run eval:vm # enquadramento do viewmodel \u2014 RODE ANTES das invariantes\nnpm run eval:invariants # as invariantes \u2014 node puro, 10-12 min\nnpm run eval:bots # botsim 60 s por mapa, sementes fixas\nnpm run eval:mat # material/luz/fog/textura nos mapas\nnpm run docs # regenera os blocos num\xe9ricos desta documenta\xe7\xe3o\nnode tools/eval/serve.mjs 8123 # servidor est\xe1tico sem Astro\n"})}),"\n",(0,a.jsxs)(s.p,{children:["E os dois quality gates, com a lista exata do que cada um roda \u2014 direto do ",(0,a.jsx)(s.code,{children:"package.json"}),":"]}),"\n","\n",(0,a.jsx)(s.pre,{children:(0,a.jsx)(s.code,{className:"language-bash",children:"npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:preload eval:docsautoria\n"})}),"\n",(0,a.jsxs)(s.p,{children:[(0,a.jsx)(s.code,{children:"package.json"})," tem ",(0,a.jsx)(s.strong,{children:"117 scripts"}),"; o motivo de cada um mora em ",(0,a.jsx)(s.code,{children:"SCRIPTS.md"})," (migrado das chaves ",(0,a.jsx)(s.code,{children:"//nome"})," em 18/08/2026) \u2014 \xe9 onde est\xe1 o porqu\xea."]}),"\n",(0,a.jsxs)(s.blockquote,{children:["\n",(0,a.jsxs)(s.p,{children:["Bloco gerado por ",(0,a.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,a.jsx)(s.code,{children:"node -p \"Object.keys(require('./package.json').scripts)\""})]}),"\n"]}),"\n","\n",(0,a.jsxs)(s.p,{children:["O ",(0,a.jsx)(s.code,{children:"check:fast"})," cobre as r\xe9guas de node puro; o CI (",(0,a.jsx)(s.code,{children:".github/workflows/ci.yml"}),") roda o\nmesmo conjunto mais os passos que exigem rede."]}),"\n",(0,a.jsxs)(s.admonition,{type:"tip",children:[(0,a.jsxs)(s.mdxAdmonitionTitle,{children:["Use o ",(0,a.jsx)(s.code,{children:"check:fast"})," no loop, o ",(0,a.jsx)(s.code,{children:"portao-browser"})," antes do PR"]}),(0,a.jsxs)(s.p,{children:["O ",(0,a.jsx)(s.code,{children:"portao-browser"})," gasta 10-12 min porque sobe o jogo cinco vezes. O ",(0,a.jsx)(s.code,{children:"check:fast"})," cobre as r\xe9guas\nque nasceram dos bugs mais recentes (menu de pausa, rodada de captura, regenera\xe7\xe3o,\nmanifesto de anima\xe7\xe3o) e roda em cerca de um minuto."]})]}),"\n",(0,a.jsx)(s.h2,{id:"onde-ir-agora",children:"Onde ir agora"}),"\n",(0,a.jsxs)(s.p,{children:["A ordem da barra lateral ",(0,a.jsx)(s.strong,{children:"\xe9"})," a ordem de leitura, e cada p\xe1gina entrega uma coisa:"]}),"\n",(0,a.jsxs)(s.ol,{children:["\n",(0,a.jsxs)(s.li,{children:[(0,a.jsx)(s.strong,{children:(0,a.jsx)(s.a,{href:"/docs/stack",children:"Stack e ferramentas"})})," \u2014 com o que isso \xe9 feito, com a vers\xe3o declarada\nde cada pe\xe7a. \xc9 onde a fronteira ",(0,a.jsx)(s.code,{children:"public/"})," \xd7 ",(0,a.jsx)(s.code,{children:"src/"})," est\xe1 explicada por inteiro."]}),"\n",(0,a.jsxs)(s.li,{children:[(0,a.jsx)(s.strong,{children:(0,a.jsx)(s.a,{href:"/docs/instrumentacao-ai",children:"Instrumenta\xe7\xe3o de IA"})})," \u2014 como o trabalho \xe9 feito aqui. Se\nvoc\xea nunca colaborou com agentes num reposit\xf3rio, comece por essa."]}),"\n",(0,a.jsxs)(s.li,{children:[(0,a.jsx)(s.strong,{children:(0,a.jsx)(s.a,{href:"/docs/quality-gates",children:"O quality gate"})})," \u2014 o que \xe9 uma invariante, como se escreve uma, as\nduas leis da casa e o teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua. ",(0,a.jsx)(s.strong,{children:"\xc9 a p\xe1gina mais \xfatil do\nsite."})]}),"\n",(0,a.jsxs)(s.li,{children:[(0,a.jsx)(s.strong,{children:(0,a.jsx)(s.a,{href:"/docs/arquitetura",children:"Arquitetura"})})," \u2014 como N agentes editam o mesmo arquivo sem\ncolidir, e a tabela de conflito. Leia antes de tocar em ",(0,a.jsx)(s.code,{children:"game.js"}),"."]}),"\n",(0,a.jsxs)(s.li,{children:[(0,a.jsx)(s.strong,{children:(0,a.jsx)(s.a,{href:"/docs/colaborar",children:"Como colaborar"})})," \u2014 o que um PR precisa pra entrar, e as ",(0,a.jsx)(s.strong,{children:"tarefas\nde primeira contribui\xe7\xe3o"})," j\xe1 escritas em\n",(0,a.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/tree/main/docs/issues",children:(0,a.jsx)(s.code,{children:"docs/issues/"})})," (com um\n",(0,a.jsx)(s.code,{children:"abrir-issues.sh"})," pronto \u2014 elas ainda n\xe3o foram abertas no GitHub)."]}),"\n",(0,a.jsxs)(s.li,{children:[(0,a.jsx)(s.strong,{children:"Licen\xe7a"})," \u2014 o ",(0,a.jsx)(s.code,{children:"LICENSE"})," na raiz declara (hoje AGPL-3.0); as superf\xedcies que\nrepetem o nome e mudam junto est\xe3o no ",(0,a.jsx)(s.code,{children:"CONTRIBUTING.md"}),"."]}),"\n",(0,a.jsxs)(s.li,{children:[(0,a.jsx)(s.strong,{children:(0,a.jsx)(s.a,{href:"/docs/estado",children:"Estado atual"})})," \u2014 fontes vivas de produ\xe7\xe3o, dados e d\xedvida conhecida\ndesde a \xfaltima medi\xe7\xe3o colada."]}),"\n"]}),"\n",(0,a.jsxs)(s.p,{children:["Para onde o projeto ",(0,a.jsx)(s.strong,{children:"vai"})," n\xe3o est\xe1 nesta documenta\xe7\xe3o: \xe9 o\n",(0,a.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,a.jsx)(s.code,{children:"docs/ROADMAP.md"})}),", e o\nplano execut\xe1vel \xe9 o\n",(0,a.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/plans/08-RELEASE-PROFISSIONAL.md",children:(0,a.jsx)(s.code,{children:"plans/08"})}),"."]})]})}function j(e={}){const{wrapper:s}={...(0,n.R)(),...e.components};return s?(0,a.jsx)(s,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},8453(e,s,r){r.d(s,{R:()=>o,x:()=>i});var d=r(6540);const a={},n=d.createContext(a);function o(e){const s=d.useContext(n);return d.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function i(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:o(e.components),d.createElement(n.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/assets/js/393ca303.bca993db.js b/public/docs/assets/js/393ca303.501859ff.js similarity index 99% rename from public/docs/assets/js/393ca303.bca993db.js rename to public/docs/assets/js/393ca303.501859ff.js index e839fd33a..4a23374f9 100644 --- a/public/docs/assets/js/393ca303.bca993db.js +++ b/public/docs/assets/js/393ca303.501859ff.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[514],{1757(e,o,a){a.r(o),a.d(o,{assets:()=>c,contentTitle:()=>i,default:()=>m,frontMatter:()=>d,metadata:()=>s,toc:()=>l});const s=JSON.parse('{"id":"colaborar","title":"Como colaborar","description":"Setup, como rodar o quality gate, o que um PR precisa, como adicionar arma / personagem / mapa, e as boas primeiras tarefas.","source":"@site/docs/colaborar.md","sourceDirName":".","slug":"/colaborar","permalink":"/docs/colaborar","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/colaborar.md","tags":[],"version":"current","sidebarPosition":6,"frontMatter":{"id":"colaborar","title":"Como colaborar","sidebar_label":"Como colaborar","sidebar_position":6,"description":"Setup, como rodar o quality gate, o que um PR precisa, como adicionar arma / personagem / mapa, e as boas primeiras tarefas."},"sidebar":"dev","previous":{"title":"Arquitetura","permalink":"/docs/arquitetura"},"next":{"title":"Estado atual","permalink":"/docs/estado"}}');var r=a(4848),n=a(8453);const d={id:"colaborar",title:"Como colaborar",sidebar_label:"Como colaborar",sidebar_position:6,description:"Setup, como rodar o quality gate, o que um PR precisa, como adicionar arma / personagem / mapa, e as boas primeiras tarefas."},i="Como colaborar",c={},l=[{value:"Setup",id:"setup",level:2},{value:"Rodar o quality gate",id:"rodar-o-quality-gate",level:2},{value:"Antes de dizer que consertou: mute",id:"antes-de-dizer-que-consertou-mute",level:3},{value:"Se o seu PR \xe9 um conserto de bug",id:"se-o-seu-pr-\xe9-um-conserto-de-bug",level:3},{value:"O que um PR precisa",id:"o-que-um-pr-precisa",level:2},{value:"1. Uma invariante nova \u2014 ou a raz\xe3o de n\xe3o precisar",id:"1-uma-invariante-nova--ou-a-raz\xe3o-de-n\xe3o-precisar",level:3},{value:"2. O quality gate n\xe3o pode piorar",id:"2-o-quality-gate-n\xe3o-pode-piorar",level:3},{value:"3. N\xfameros, com arquivo:linha",id:"3-n\xfameros-com-arquivolinha",level:3},{value:"4. Uma frente por PR",id:"4-uma-frente-por-pr",level:3},{value:"5. Higiene do reposit\xf3rio",id:"5-higiene-do-reposit\xf3rio",level:3},{value:"6. Linha editorial",id:"6-linha-editorial",level:3},{value:"Como adicionar uma arma",id:"como-adicionar-uma-arma",level:2},{value:"Como adicionar um personagem",id:"como-adicionar-um-personagem",level:2},{value:"Como adicionar um mapa",id:"como-adicionar-um-mapa",level:2},{value:"Boas primeiras tarefas",id:"boas-primeiras-tarefas",level:2},{value:"Muito boas para o primeiro PR",id:"muito-boas-para-o-primeiro-pr",level:3},{value:"Trabalho de verdade, ainda acess\xedvel",id:"trabalho-de-verdade-ainda-acess\xedvel",level:3},{value:"Alto valor, precisa de conversa antes",id:"alto-valor-precisa-de-conversa-antes",level:3},{value:"Processo",id:"processo",level:2}];function t(e){const o={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,n.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(o.header,{children:(0,r.jsx)(o.h1,{id:"como-colaborar",children:"Como colaborar"})}),"\n",(0,r.jsxs)(o.p,{children:["O n\xfamero abaixo n\xe3o \xe9 ret\xf3rica, e n\xe3o \xe9 escrito \xe0 m\xe3o: sai de ",(0,r.jsx)(o.code,{children:"git shortlog -sn --no-merges"})," descontando os autores que s\xe3o agentes de IA (que assinam como ",(0,r.jsx)(o.code,{children:"Claude"})," /\n",(0,r.jsx)(o.code,{children:"Claude (gauntlet \u2026)"}),")."]}),"\n","\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"11 identidades de autoria humana"})," assinam commit no hist\xf3rico ",(0,r.jsx)(o.strong,{children:"desta branch"}),": ",(0,r.jsx)(o.code,{children:"ruben-cytonic"}),", ",(0,r.jsx)(o.code,{children:"Emerson Garrido"}),", ",(0,r.jsx)(o.code,{children:"Ruben"}),", ",(0,r.jsx)(o.code,{children:"rubenmarcus"}),", ",(0,r.jsx)(o.code,{children:"Ruben Marcus"}),", ",(0,r.jsx)(o.code,{children:"William Oliveira"}),", ",(0,r.jsx)(o.code,{children:"Juan Versolato Lopes"}),", ",(0,r.jsx)(o.code,{children:"daeeseD"}),", ",(0,r.jsx)(o.code,{children:"matheusgb"}),", ",(0,r.jsx)(o.code,{children:"Man\xe1 Soares"}),", ",(0,r.jsx)(o.code,{children:"daltonfontes"}),". O resto dos commits \xe9 assinado por agentes de IA. Branch n\xe3o \xe9 reposit\xf3rio: quem contribuiu num ramo que esta branch n\xe3o cont\xe9m ",(0,r.jsx)(o.strong,{children:"n\xe3o aparece aqui"}),"."]}),"\n",(0,r.jsxs)(o.blockquote,{children:["\n",(0,r.jsxs)(o.p,{children:["Bloco gerado por ",(0,r.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,r.jsx)(o.code,{children:"git shortlog -sn --no-merges (descontando autores que s\xe3o agentes)"})]}),"\n"]}),"\n","\n",(0,r.jsx)(o.p,{children:"N\xe3o existe time, n\xe3o existe comunidade, n\xe3o existe fila de revisores \u2014 existem essas\npessoas e um quality gate automatizado."}),"\n",(0,r.jsx)(o.admonition,{title:"O bloco acima conta a BRANCH, e o projeto \xe9 maior que ela",type:"note",children:(0,r.jsxs)(o.p,{children:["A ",(0,r.jsx)(o.code,{children:"main"})," tem um quarto contribuidor que esta branch de trabalho n\xe3o cont\xe9m \u2014 13 commits\nde um cliente desktop, mesclados em julho. Quem, quanto e por que isso importa para\nqualquer decis\xe3o de licen\xe7a est\xe1 no ",(0,r.jsx)(o.code,{children:"CONTRIBUTING.md"})," (se\xe7\xe3o de licen\xe7a e superf\xedcies)."]})}),"\n",(0,r.jsxs)(o.p,{children:["Isso \xe9 relevante pra voc\xea de duas formas opostas. A ruim: se o seu PR travar, pode\ndemorar. A boa: ",(0,r.jsx)(o.strong,{children:"quase toda a r\xe9gua \xe9 m\xe1quina."})," ",(0,r.jsx)(o.code,{children:"npm run check:fast"})," te d\xe1 o mesmo veredito\nque o mantenedor daria, antes de voc\xea abrir o PR, sem esperar ningu\xe9m. A barreira \xe9 baixa\n",(0,r.jsx)(o.strong,{children:"de prop\xf3sito"})," \u2014 \xe9 um dos princ\xedpios que n\xe3o mudam do\n",(0,r.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,r.jsx)(o.code,{children:"docs/ROADMAP.md"})}),".\nMas a r\xe9gua n\xe3o \xe9."]}),"\n",(0,r.jsxs)(o.p,{children:["Resumo em uma frase: ",(0,r.jsx)(o.strong,{children:"traga o n\xfamero."})," Um PR que muda comportamento vis\xedvel e n\xe3o traz\nnem uma invariante nova nem a raz\xe3o de n\xe3o precisar de uma vai voltar com uma pergunta."]}),"\n",(0,r.jsx)(o.h2,{id:"setup",children:"Setup"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"git clone https://github.com/rubenmarcus/csbrasil.git && cd csbrasil\nnpm install\nnpm run dev # http://localhost:4321 \u2014 a rota raiz J\xc1 \xc9 o jogo\n"})}),"\n",(0,r.jsx)(o.p,{children:"Opcional:"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"npm run fetch-audio # pacote de \xe1udio (sem ele: sons sintetizados)\n"})}),"\n",(0,r.jsxs)(o.p,{children:["Requisitos: Node 22 (\xe9 o que o CI usa, ",(0,r.jsx)(o.code,{children:".github/workflows/ci.yml:14"}),") e Python 3 para\nparte do arn\xeas (",(0,r.jsx)(o.code,{children:"ref-measure.py"}),", ",(0,r.jsx)(o.code,{children:"char_probe.py"}),", ",(0,r.jsx)(o.code,{children:"mat_shade.py"})," \u2014 usam numpy e PIL)."]}),"\n",(0,r.jsxs)(o.admonition,{type:"caution",children:[(0,r.jsxs)(o.mdxAdmonitionTitle,{children:["Servir ",(0,r.jsx)(o.code,{children:"public/"})," N\xc3O roda o jogo"]}),(0,r.jsxs)(o.p,{children:["N\xe3o existe ",(0,r.jsx)(o.code,{children:"public/index.html"}),": o HTML do jogo \xe9 ",(0,r.jsx)(o.code,{children:"src/pages/index.astro"}),", na rota raiz.\nUse ",(0,r.jsx)(o.code,{children:"npm run dev"}),". Detalhes e prova em\n",(0,r.jsx)(o.a,{href:"/docs/#a-pegadinha-que-custa-a-primeira-hora-de-todo-mundo",children:"Come\xe7ando"}),"."]})]}),"\n",(0,r.jsx)(o.h2,{id:"rodar-o-quality-gate",children:"Rodar o quality gate"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"npm run eval:vm # OBRIGAT\xd3RIO ANTES \u2014 ver o aviso abaixo\nnode tools/eval/invariants.mjs # o quality gate inteiro\nnode tools/eval/invariants.mjs --json # sa\xedda pra m\xe1quina\nnpm run check:fast # syntax + quality gate (r\xe9guas de node puro)\n"})}),"\n",(0,r.jsxs)(o.admonition,{type:"danger",children:[(0,r.jsxs)(o.mdxAdmonitionTitle,{children:[(0,r.jsx)(o.code,{children:"eval:vm"})," roda ANTES de ",(0,r.jsx)(o.code,{children:"invariants.mjs"}),". Sempre."]}),(0,r.jsxs)(o.p,{children:["As invariantes de viewmodel (VM1\u2013VM19) ",(0,r.jsx)(o.strong,{children:"leem"})," ",(0,r.jsx)(o.code,{children:"tools/eval/vm_mint_audit.json"}),", que \xe9 o\n",(0,r.jsx)(o.code,{children:"eval:vm"})," quem ",(0,r.jsx)(o.strong,{children:"escreve"}),". Rodar as invariantes com esse JSON velho mede o viewmodel de\nontem e ",(0,r.jsx)(o.strong,{children:"inventa vermelha"}),": em 04/08/2026 o JSON estava em ",(0,r.jsx)(o.code,{children:"V0=80\xb0"})," contra o ",(0,r.jsx)(o.code,{children:"game.js"}),"\nem ",(0,r.jsx)(o.code,{children:"V0=42\xb0"}),", e a VM5 acusava ",(0,r.jsx)(o.strong,{children:"26/26 armas fora"}),"; depois de ",(0,r.jsx)(o.code,{children:"npm run eval:vm"}),", ",(0,r.jsx)(o.strong,{children:"3/26"}),".\nA VM1 caiu de 26/26 para 2/26 e a VM9 ficou verde."]}),(0,r.jsxs)(o.p,{children:["A ordem do ",(0,r.jsx)(o.code,{children:"npm run check"})," j\xe1 foi corrigida (",(0,r.jsx)(o.code,{children:"package.json"}),") \u2014 o cuidado \xe9 para quando\nvoc\xea chamar ",(0,r.jsx)(o.code,{children:"node tools/eval/invariants.mjs"})," na m\xe3o. Detalhe: BUG-02 do\n",(0,r.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/KNOWN-BUGS.md",children:(0,r.jsx)(o.code,{children:"KNOWN-BUGS.md"})}),"."]})]}),"\n",(0,r.jsxs)(o.p,{children:["Custo real: numa m\xe1quina de 2 CPUs, ",(0,r.jsx)(o.strong,{children:"cerca de 10 minutos"}),". Ele sobe o jogo real cinco\nvezes (uma por mapa), roda 60 s de simula\xe7\xe3o de bot por mapa e audita todos os GLBs de arma.\nRode antes de abrir o PR, n\xe3o depois de receber a review."]}),"\n",(0,r.jsx)(o.p,{children:"Arn\xeases individuais, quando voc\xea quiser iterar r\xe1pido numa frente s\xf3:"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"node tools/eval/vm-mint-audit.mjs # enquadramento de viewmodel (todo o arsenal)\nnode tools/eval/vm-solve.mjs # existe ponto vi\xe1vel pras invariantes de VM?\nnode tools/eval/vm-solve.mjs --atual # s\xf3 as margens da config atual (instant\xe2neo)\nnode tools/eval/botsim.mjs 60 all # navega\xe7\xe3o de bots, todos os mapas, sementes fixas\nnode tools/eval/char-probe.mjs # personagens (C1..C6)\nnode tools/eval/map-check.mjs all # geometria de mapa (MAP1-MAP3, CTF1)\nnode tools/eval/mat-check.mjs # material/luz/fog/textura\nnode tools/eval/pickup-check.mjs # todo pickup \xe9 alcan\xe7\xe1vel?\nnode tools/eval/ui-check.mjs # UI1 contraste \xb7 UI2 polui\xe7\xe3o \xb7 UI3 \xe1rea morta \xb7 UI4 ritmo\n"})}),"\n",(0,r.jsx)(o.h3,{id:"antes-de-dizer-que-consertou-mute",children:"Antes de dizer que consertou: mute"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"MUT=ui1_ctf_scrim_fraco node tools/eval/ui-check.mjs # espera UI1 VERMELHA\n"})}),"\n",(0,r.jsxs)(o.p,{children:["Desfa\xe7a a sua pr\xf3pria corre\xe7\xe3o e confira que o quality gate ",(0,r.jsx)(o.strong,{children:"fica vermelho"}),". Se ficar verde,\no que voc\xea mediu n\xe3o \xe9 o que voc\xea consertou. \xc9 a li\xe7\xe3o mais cara deste reposit\xf3rio e ela\ntem uma p\xe1gina inteira: ",(0,r.jsx)(o.a,{href:"/docs/quality-gates#teste-de-muta%C3%A7%C3%A3o-da-pr%C3%B3pria-r%C3%A9gua",children:"Teste de muta\xe7\xe3o"}),"."]}),"\n",(0,r.jsx)(o.h3,{id:"se-o-seu-pr-\xe9-um-conserto-de-bug",children:"Se o seu PR \xe9 um conserto de bug"}),"\n",(0,r.jsxs)(o.p,{children:["Use a skill ",(0,r.jsx)(o.code,{children:"bug-hunt"})," (",(0,r.jsx)(o.code,{children:".claude/skills/bug-hunt/SKILL.md"}),"). Ela \xe9 o passo a passo desta\ndoutrina aplicado a defeito \u2014 com o caso real que comprou cada regra, o gabarito da entrada\ndo ",(0,r.jsx)(o.code,{children:"KNOWN-BUGS.md"})," e o do relat\xf3rio final, incluindo como declarar o que voc\xea ",(0,r.jsx)(o.strong,{children:"n\xe3o"}),"\nverificou. Serve para agente e para gente."]}),"\n",(0,r.jsx)(o.h2,{id:"o-que-um-pr-precisa",children:"O que um PR precisa"}),"\n",(0,r.jsx)(o.h3,{id:"1-uma-invariante-nova--ou-a-raz\xe3o-de-n\xe3o-precisar",children:"1. Uma invariante nova \u2014 ou a raz\xe3o de n\xe3o precisar"}),"\n",(0,r.jsxs)(o.p,{children:["Esta \xe9 a regra que define o projeto. Todo PR que muda ",(0,r.jsx)(o.strong,{children:"comportamento observ\xe1vel"})," traz\numa das duas coisas:"]}),"\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Uma invariante nova"})," em ",(0,r.jsx)(o.code,{children:"tools/eval/invariants.mjs"}),", com teto que tem proced\xeancia\n(arquivo de refer\xeancia + pixel medido + script que reproduz), ou"]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Uma frase na descri\xe7\xe3o do PR"}),' dizendo por que n\xe3o precisa. Raz\xf5es v\xe1lidas: "j\xe1 \xe9\ncoberto pela invariante X" (diga qual), "\xe9 refatora\xe7\xe3o sem mudan\xe7a observ\xe1vel \u2014 o\nquality gate d\xe1 o mesmo placar antes e depois" (cole os dois), "\xe9 conte\xfado puro (texto,\nasset) sem regra de jogo associada".']}),"\n"]}),"\n",(0,r.jsx)(o.p,{children:'Raz\xe3o inv\xe1lida: "testei manualmente e ficou bom".'}),"\n",(0,r.jsxs)(o.p,{children:["Por qu\xea: ",(0,r.jsx)(o.strong,{children:"inten\xe7\xe3o que n\xe3o vira invariante \xe9 otimizada para fora"}),". Uma rodada levou o\nquality gate de 16/21 para 19/21 sem afrouxar um teto sequer, e foi reprovada, porque destruiu\nem sil\xeancio uma decis\xe3o est\xe9tica que nenhuma invariante codificava. Caso completo em\n",(0,r.jsx)(o.a,{href:"/docs/quality-gates#lei-1--inten%C3%A7%C3%A3o-que-n%C3%A3o-vira-invariante-%C3%A9-otimizada-para-fora",children:"O quality gate"}),"."]}),"\n",(0,r.jsx)(o.h3,{id:"2-o-quality-gate-n\xe3o-pode-piorar",children:"2. O quality gate n\xe3o pode piorar"}),"\n",(0,r.jsxs)(o.p,{children:["Cole a sa\xedda de ",(0,r.jsx)(o.code,{children:"node tools/eval/invariants.mjs"})," antes e depois. Se alguma cr\xedtica ficou\nvermelha, o PR n\xe3o entra. Se voc\xea ",(0,r.jsx)(o.strong,{children:"consertou"})," uma vermelha, diga qual e mostre."]}),"\n",(0,r.jsxs)(o.p,{children:["O estado do quality gate deve ser medido (",(0,r.jsx)(o.a,{href:"/docs/estado",children:"veja como"}),"); a lista viva com causa raiz est\xe1\nno ",(0,r.jsx)(o.code,{children:"KNOWN-BUGS.md"}),"). Isso n\xe3o \xe9 licen\xe7a para piorar: o compromisso \xe9 ",(0,r.jsx)(o.em,{children:'"a sua mudan\xe7a n\xe3o\nacrescenta vermelho"'}),"."]}),"\n",(0,r.jsxs)(o.h3,{id:"3-n\xfameros-com-arquivolinha",children:["3. N\xfameros, com ",(0,r.jsx)(o.code,{children:"arquivo:linha"})]}),"\n",(0,r.jsxs)(o.p,{children:['A afirma\xe7\xe3o "melhorei a ilumina\xe7\xe3o" n\xe3o \xe9 revis\xe1vel. "O ch\xe3o do ',(0,r.jsx)(o.code,{children:"praca_poderes"})," estava 8 pontos\nde L* acima das paredes, causa em ",(0,r.jsx)(o.code,{children:"map_brasilia.js:NNN"}),', corrigido para X" \xe9. Essa\nexig\xeancia n\xe3o \xe9 estilo \u2014 \xe9 o que permite que a pr\xf3xima rodada confira o seu trabalho.']}),"\n",(0,r.jsx)(o.h3,{id:"4-uma-frente-por-pr",children:"4. Uma frente por PR"}),"\n",(0,r.jsxs)(o.p,{children:["Consulte a tabela de conflito (",(0,r.jsx)(o.a,{href:"/docs/arquitetura#a-tabela-de-conflito",children:"Arquitetura"}),"). Um\nPR que toca armas + UI + mapa \xe9 tr\xeas PRs escondidos, e vai colidir com tr\xeas frentes. Em\n",(0,r.jsx)(o.code,{children:"game.js"}),", edite por trecho; nunca sobrescreva o arquivo inteiro."]}),"\n",(0,r.jsx)(o.h3,{id:"5-higiene-do-reposit\xf3rio",children:"5. Higiene do reposit\xf3rio"}),"\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"node --check"})," em cada arquivo de ",(0,r.jsx)(o.code,{children:"public/js/"})," que voc\xea editou (o CI faz isso primeiro,\n",(0,r.jsx)(o.code,{children:".github/workflows/ci.yml:19-20"}),")."]}),"\n",(0,r.jsxs)(o.li,{children:["Coment\xe1rio ",(0,r.jsx)(o.strong,{children:"em portugu\xeas explicando o porqu\xea"}),", n\xe3o o qu\xea. \xc9 a cultura do repo e \xe9 o\nque sobrevive ao pr\xf3ximo handoff."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Nunca delete coment\xe1rio de proced\xeancia"})," num PR de limpeza. Aquele par\xe1grafo longo\nexplicando de onde veio o n\xfamero 0,513 \xe9 o que impede a pr\xf3xima rodada de repetir tr\xeas\ndias perdidos."]}),"\n",(0,r.jsxs)(o.li,{children:["Mexeu em ",(0,r.jsx)(o.code,{children:"public/js/*.js"}),"? ",(0,r.jsxs)(o.strong,{children:["Bump o ",(0,r.jsx)(o.code,{children:"?v="})," nos dois lados"]})," \u2014 ",(0,r.jsx)(o.code,{children:"public/js/version.js"})," e o\nimport map de ",(0,r.jsx)(o.code,{children:"src/pages/index.astro"}),'. J\xe1 custou dias de "corre\xe7\xe3o que n\xe3o chegava".']}),"\n",(0,r.jsxs)(o.li,{children:["Mexeu em ",(0,r.jsx)(o.code,{children:"public/js/*.js"}),", no ",(0,r.jsx)(o.code,{children:"maps.js"}),", no ",(0,r.jsx)(o.code,{children:"characters.js"})," ou numa depend\xeancia?\n",(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"npm run docs"})]})," e commite junto. O ",(0,r.jsx)(o.code,{children:"docs:check"})," est\xe1 no ",(0,r.jsx)(o.code,{children:"check:fast"})," e vai\nreprovar \u2014 leva menos de um segundo e \xe9 o que impede a doc de voltar a mentir."]}),"\n",(0,r.jsxs)(o.li,{children:["Nada de asset com copyright. Nada de ",(0,r.jsx)(o.code,{children:"service_role"})," key commitada."]}),"\n",(0,r.jsx)(o.li,{children:"Nada de depend\xeancia de runtime no jogo. Three.js \xe9 vendorizado; o jogo tem que rodar\narrastando a pasta pra um host est\xe1tico."}),"\n"]}),"\n",(0,r.jsx)(o.h3,{id:"6-linha-editorial",children:"6. Linha editorial"}),"\n",(0,r.jsxs)(o.p,{children:["De ",(0,r.jsx)(o.code,{children:"CONTRIBUTING.md:7-16"}),": o jogo n\xe3o tem lado pol\xedtico (os dois times t\xeam a mesma\nmec\xe2nica), n\xe3o incita \xf3dio, n\xe3o usa pessoas reais \u2014 s\xf3 arqu\xe9tipos originais, sem gore.\nContribui\xe7\xf5es que violem isso s\xe3o recusadas. N\xe3o \xe9 burocracia: \xe9 o que protege o projeto\nde takedown e de virar outra coisa."]}),"\n",(0,r.jsx)(o.h2,{id:"como-adicionar-uma-arma",children:"Como adicionar uma arma"}),"\n",(0,r.jsxs)(o.p,{children:["O pipeline \xe9 data-driven a partir do GLB. Os GLBs de arma vivem em ",(0,r.jsx)(o.code,{children:"public/models/weapons/"}),"\n(a contagem est\xe1 no bloco gerado de ",(0,r.jsx)(o.a,{href:"/docs/",children:"Come\xe7ando"}),")."]}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Coloque o GLB"})," em ",(0,r.jsx)(o.code,{children:"public/models/weapons/.glb"}),". S\xf3 geometria \u2014 o material vem\ndo pipeline (",(0,r.jsx)(o.code,{children:"MAT1"})," exige ",(0,r.jsx)(o.code,{children:"metallicFactor 1 / roughnessFactor 1"})," com mapa\nmetallicRoughness, que \xe9 o padr\xe3o de todas as atuais)."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Declare a arma"})," em ",(0,r.jsx)(o.code,{children:"public/js/weapons.js"}),". Os campos que o quality gate l\xea:","\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"len"})," \u2014 comprimento em metros. ",(0,r.jsxs)(o.strong,{children:[(0,r.jsx)(o.code,{children:"ARM4"})," reprova acima de 1,25 m"]})," fora de sniper de\nferrolho. \xc9 o campo que normaliza a escala; n\xe3o \xe9 decora\xe7\xe3o."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"gripZ"})," \u2014 fra\xe7\xe3o do comprimento, contada ",(0,r.jsx)(o.strong,{children:"a partir da boca"}),", onde fica o grip\n(ak/m4 usam 0,62 \u2014 cai no guarda-mato). \xc9 o que ancora a m\xe3o."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"vm"})," \u2014 multiplicador de escala do mesh no viewmodel. Existe porque a ",(0,r.jsx)(o.code,{children:"m92"})," batia\n14,50% contra o teto medido de 13,09% da VM18b."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"scope: true"})," ",(0,r.jsx)(o.strong,{children:"exige"})," ",(0,r.jsx)(o.code,{children:"spreadScope"})," declarado \u2014 \xe9 a ",(0,r.jsx)(o.code,{children:"ARM1"}),', e ela existe por causa\ndo "sniper sem zoom".']}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Rode o auditor:"})," ",(0,r.jsx)(o.code,{children:"node tools/eval/vm-mint-audit.mjs"}),". Ele abre o GLB com parser\npr\xf3prio, projeta o viewmodel nos dois aspectos e escreve ",(0,r.jsx)(o.code,{children:"tools/eval/vm_mint_audit.json"}),".\n",(0,r.jsx)(o.strong,{children:"Esse JSON \xe9 versionado"})," \u2014 sem ele, VM1\u2013VM6/VM9/VM10 viram PULADAS, que \xe9 quality gate\nverde por aus\xeancia de dado (",(0,r.jsx)(o.code,{children:".github/workflows/ci.yml:24-27"}),")."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Rode o quality gate."})," Voc\xea vai enfrentar VM1, VM3, VM5, VM9, VM12, VM16, VM18, VM18b,\nVM19 \u2014 nove invariantes de enquadramento, todas com faixa medida em frame de\nrefer\xeancia. Se n\xe3o fechar, use ",(0,r.jsx)(o.code,{children:"node tools/eval/vm-solve.mjs"})," em vez de tunar no olho:\nele l\xea os tetos do pr\xf3prio ",(0,r.jsx)(o.code,{children:"invariants.mjs"})," e diz se existe ponto vi\xe1vel, ou ",(0,r.jsx)(o.strong,{children:"qual par\nde invariantes se cruza vazio e por quanto"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Commite o ",(0,r.jsx)(o.code,{children:"vm_mint_audit.json"})," atualizado"]})," junto com o resto."]}),"\n"]}),"\n",(0,r.jsx)(o.h2,{id:"como-adicionar-um-personagem",children:"Como adicionar um personagem"}),"\n",(0,r.jsxs)(o.p,{children:["45 GLBs em ",(0,r.jsx)(o.code,{children:"public/models/characters/"}),", 44 medidos pelo ",(0,r.jsx)(o.code,{children:"char-probe.mjs"}),"."]}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"GLB com rig"}),", na bind pose, p\xe9s no ch\xe3o. ",(0,r.jsx)(o.code,{children:"CHR3"})," exige ",(0,r.jsx)(o.code,{children:"|base da bbox| \u2264 0,01 m"})," na\nbind pose ",(0,r.jsx)(o.strong,{children:"e em cada clipe"})," \u2014 o sinal separa dois defeitos: ",(0,r.jsx)(o.code,{children:"y < 0"})," \xe9 p\xe9 dentro do\nch\xe3o, ",(0,r.jsx)(o.code,{children:"y > 0"})," \xe9 boneco no ar."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Declare"})," em ",(0,r.jsx)(o.code,{children:"public/js/characters.js"})," / ",(0,r.jsx)(o.code,{children:"public/js/glbchars.js"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"node tools/eval/char-probe.mjs"}),"."]})," O que ele vai cobrar:","\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR1"}),' \u2014 propor\xe7\xe3o antropom\xe9trica e \xedndice de "bal\xe3o". Hoje ',(0,r.jsx)(o.strong,{children:"est\xe1 vermelha para o\nelenco inteiro"}),", ent\xe3o n\xe3o \xe9 voc\xea que a quebrou; mas n\xe3o a piore."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR2"})," \u2014 altura do corpo dentro de meia hitbox de cabe\xe7a (dispers\xe3o \u2264 0,15 m). Medida\n",(0,r.jsx)(o.strong,{children:"sem adere\xe7o"}),": chap\xe9u/cabelo/mastro inflam a bbox e fazem o caminho GLB (a evid\xeancia da pr\xf3pria CHR2 aponta ",(0,r.jsx)(o.code,{children:"glbchars.js:319-322"}),")\nencolher o corpo."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR4"})," \u2014 nenhuma palma nasce enterrada no corpo."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR5"}),"/",(0,r.jsx)(o.code,{children:"CHR5B"})," \u2014 acabamento (normal + roughness + AO). A CHR5B ",(0,r.jsx)(o.strong,{children:"ficou verde em\n04/08"}),': era o "tr\xeas n\xedveis de acabamento na mesma tela" que o dono descreveu, com\nboa parte do elenco sem nenhum mapa de superf\xedcie, e hoje \xe9 zero personagem sem.\nPersonagem novo ',(0,r.jsx)(o.strong,{children:"sem"})," normal + roughness reabre a vermelha \u2014 traga os mapas."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR6"})," \u2014 nenhum par com a mesma silhueta (IoU \u2264 0,98)."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(o.h2,{id:"como-adicionar-um-mapa",children:"Como adicionar um mapa"}),"\n",(0,r.jsxs)(o.p,{children:["Hoje mapas s\xe3o ",(0,r.jsx)(o.strong,{children:"c\xf3digo"}),", n\xe3o dado: cada ",(0,r.jsx)(o.code,{children:"map_*.js"})," \xe9 geometria declarada \xe0 m\xe3o, e os\nmaiores rivalizam em tamanho com os m\xf3dulos de sistema. Migrar isso para JSON \xe9 a Fase 2\nconte\xfado como dado do\n",(0,r.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,r.jsx)(o.code,{children:"docs/ROADMAP.md"})}),", e \xe9 a\ncontribui\xe7\xe3o de maior alavancagem do projeto."]}),"\n",(0,r.jsxs)(o.p,{children:["O registro, gerado do ",(0,r.jsx)(o.code,{children:"MAPS"})," de ",(0,r.jsx)(o.code,{children:"public/js/maps.js"}),":"]}),"\n","\n",(0,r.jsxs)(o.table,{children:[(0,r.jsx)(o.thead,{children:(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.th,{children:"Id"}),(0,r.jsx)(o.th,{children:"Nome no menu"}),(0,r.jsx)(o.th,{children:"Abre em"}),(0,r.jsxs)(o.th,{children:["Arquivo em ",(0,r.jsx)(o.code,{children:"public/js/"})]}),(0,r.jsx)(o.th,{style:{textAlign:"right"},children:"Linhas"})]})}),(0,r.jsxs)(o.tbody,{children:[(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"praca_poderes"})}),(0,r.jsx)(o.td,{children:"Pra\xe7a dos Tr\xeas Poderes"}),(0,r.jsx)(o.td,{children:"rodadas"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_brasilia.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"1.830"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"piscina_treta"})}),(0,r.jsx)(o.td,{children:"Piscina da Treta"}),(0,r.jsx)(o.td,{children:"rodadas"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_piscina.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"810"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"loja_h"})}),(0,r.jsx)(o.td,{children:"Loja H (Estacionamento)"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_havan.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"1.964"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"ferro_velho"})}),(0,r.jsx)(o.td,{children:"Ferro Velho do Z\xe9"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_ferrovelho.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"1.888"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"quebrada"})}),(0,r.jsx)(o.td,{children:"Quebrada (Rua do Baile)"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_quebrada.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"1.599"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"posto_treta"})}),(0,r.jsx)(o.td,{children:"Posto da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_posto.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"489"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"upa_24h"})}),(0,r.jsx)(o.td,{children:"UPA 24h da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_upa.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"288"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"obras_prefeitura"})}),(0,r.jsx)(o.td,{children:"Obras da Prefeitura"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_obras.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"240"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"atacadao_treta"})}),(0,r.jsx)(o.td,{children:"Atacad\xe3o da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_atacadao.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"255"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"parque_treta"})}),(0,r.jsx)(o.td,{children:"Parque da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_parque.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"402"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"velho_oeste"})}),(0,r.jsx)(o.td,{children:"Velho Oeste da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_velho_oeste.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"433"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"penitenciaria"})}),(0,r.jsx)(o.td,{children:"Penitenci\xe1ria da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_penitenciaria.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"247"})]})]})]}),"\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"12 mapas registrados"})," \u2014 2 abrem em rodadas e 10 em captura. ",(0,r.jsx)(o.code,{children:"ctfMode"})," ",(0,r.jsx)(o.strong,{children:"abre"})," o mapa em captura, n\xe3o prende: o jogador troca no menu (\xe9 a ",(0,r.jsx)(o.code,{children:"MOD1"}),"). H\xe1 14 arquivos ",(0,r.jsx)(o.code,{children:"map_*.js"})," em ",(0,r.jsx)(o.code,{children:"public/js/"})," \u2014 arquivo no disco ",(0,r.jsx)(o.strong,{children:"n\xe3o"})," implica mapa jog\xe1vel."]}),"\n",(0,r.jsxs)(o.blockquote,{children:["\n",(0,r.jsxs)(o.p,{children:["Bloco gerado por ",(0,r.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,r.jsx)(o.code,{children:"objeto MAPS de public/js/maps.js"})]}),"\n"]}),"\n","\n",(0,r.jsx)(o.p,{children:"Dois avisos que custam tempo se voc\xea n\xe3o souber:"}),"\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:[(0,r.jsx)(o.code,{children:"praca_old"}),' ("Pra\xe7a (cl\xe1ssico)") N\xc3O existe mais.']})," Saiu do registro e o\n",(0,r.jsx)(o.code,{children:"public/js/map.js"})," foi apagado junto (pedido literal do dono: ",(0,r.jsx)(o.em,{children:'"vamos apagar pra\xe7a\ncl\xe1ssica"'}),"). Se voc\xea encontrar ",(0,r.jsx)(o.code,{children:"praca_old"})," numa sa\xedda de r\xe9gua, essa sa\xedda \xe9 anterior \xe0\nremo\xe7\xe3o \u2014 \xe9 o caso do hist\xf3rico explicado em ",(0,r.jsx)(o.a,{href:"/docs/estado",children:"Estado atual"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:[(0,r.jsx)(o.code,{children:"map_piscinao_ramos.js"})," existe no disco e N\xc3O est\xe1 no registro"]}),' (\xe9 a vers\xe3o "Piscin\xe3o",\nfora do menu). Arquivo de mapa em ',(0,r.jsx)(o.code,{children:"public/js/"})," n\xe3o implica mapa jog\xe1vel; quem decide \xe9\no objeto ",(0,r.jsx)(o.code,{children:"MAPS"}),"."]}),"\n"]}),"\n",(0,r.jsx)(o.p,{children:"Para adicionar um mapa no formato de hoje:"}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Crie ",(0,r.jsx)(o.code,{children:"public/js/map_.js"})]})," exportando uma fun\xe7\xe3o ",(0,r.jsx)(o.code,{children:"build()"}),". Use\n",(0,r.jsx)(o.code,{children:"map_piscina.js"})," como refer\xeancia \u2014 \xe9 o menor dos registrados (a tabela acima traz o\ntamanho de cada um)."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Registre em ",(0,r.jsx)(o.code,{children:"public/js/maps.js:8-36"})]})," \u2014 nome exibido, ",(0,r.jsx)(o.code,{children:"build"}),", e ",(0,r.jsx)(o.code,{children:"ctfMode: true"})," se\na geometria foi desenhada em volta de bandeiras. ",(0,r.jsx)(o.code,{children:"ctfMode"})," ",(0,r.jsx)(o.strong,{children:"abre"})," o mapa em captura;\nn\xe3o prende. ",(0,r.jsx)(o.strong,{children:"N\xe3o"})," existe mais ",(0,r.jsx)(o.code,{children:"ctfOnly"}),": ",(0,r.jsx)(o.code,{children:"MOD1"})," reprova qualquer mapa que force o\nmodo. O jogador escolhe."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"node tools/eval/map-check.mjs "}),"."]})," O que ele mede, tudo por raycast\ncontra o mundo real:","\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"MAP1"}),' \u2014 nenhum spawn e nenhum ch\xe3o and\xe1vel com o corpo dentro de geometria s\xf3lida.\nTeto = degrau de 0,30 m (acima disso n\xe3o \xe9 "passar por cima", \xe9 "estar dentro").']}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"MAP2"})," \u2014 cada time nasce todo no mesmo andar; respawn n\xe3o vis\xedvel de fora (medido com\no ",(0,r.jsx)(o.code,{children:"_losClear"})," ",(0,r.jsx)(o.strong,{children:"do jogo"}),", a mesma fun\xe7\xe3o que decide se o bot atira em voc\xea)."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"MAP3"})," \u2014 escada dentro da NBR 9077 / Blondel (espelho 16\u201318 cm, piso 25\u201332 cm,\n2h+p 63\u201365 cm, largura \u2265 1,20 m) ",(0,r.jsx)(o.strong,{children:"e"})," o grafo de navega\xe7\xe3o + o flood-fill sobem por\nela."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CTF1"})," \u2014 bandeiras n\xe3o colineares, \u2265 2 raios do spawn mais pr\xf3ximo, nenhuma enterrada."]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"node tools/eval/pickup-check.mjs"})]})," (alimenta a ",(0,r.jsx)(o.code,{children:"VM14"}),"): todo pickup precisa\nser alcan\xe7\xe1vel ",(0,r.jsx)(o.strong,{children:"a p\xe9"}),", por flood-fill de conectividade real em grade de 0,25 m\nsemeado nos spawns dos dois times. J\xe1 aconteceu de armas ca\xedrem dentro da piscina do\n",(0,r.jsx)(o.code,{children:"piscina_treta"})," com o quality gate marcando v\xe3o ",(0,r.jsx)(o.strong,{children:"0,0000 \u2014 VERDE"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"node tools/eval/botsim.mjs 60 "})]}),": os bots precisam navegar o seu mapa\nsem travar (",(0,r.jsx)(o.code,{children:"BOT3"})," stuck \u2264 4%), sem andar de lado (",(0,r.jsx)(o.code,{children:"BOT1"}),") e sem girar parados (",(0,r.jsx)(o.code,{children:"BOT2"}),').\nWaypoint desconexo \xe9 o defeito mais comum de mapa novo, e j\xe1 quebrou PRs antes\n(\xe9 o defeito que a dire\xe7\xe3o "conte\xfado como dado" existe para matar).']}),"\n"]}),"\n",(0,r.jsx)(o.h2,{id:"boas-primeiras-tarefas",children:"Boas primeiras tarefas"}),"\n",(0,r.jsx)(o.p,{children:"Ordenadas por (impacto \xf7 esfor\xe7o). Todas s\xe3o reais, verificadas nesta \xe1rvore, e nenhuma\nexige entender o jogo inteiro."}),"\n",(0,r.jsx)(o.h3,{id:"muito-boas-para-o-primeiro-pr",children:"Muito boas para o primeiro PR"}),"\n",(0,r.jsxs)(o.p,{children:["As tarefas de entrada moram em ",(0,r.jsx)(o.strong,{children:(0,r.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/tree/main/docs/issues",children:(0,r.jsx)(o.code,{children:"docs/issues/"})})}),",\numa por arquivo, cada uma com contexto, o que fazer, crit\xe9rio de aceite e quais arquivos\ntocar. O ",(0,r.jsx)(o.code,{children:"README.md"})," de l\xe1 indexa por tempo dispon\xedvel (30 min / 1 h / 2-3 h) e por \xe1rea\n(SEO, UI, backend, CI). ",(0,r.jsxs)(o.strong,{children:["Nenhuma delas exige tocar em ",(0,r.jsx)(o.code,{children:"public/js/*.js"})]}),", de prop\xf3sito:\n\xe9 o c\xf3digo onde os agentes de gameplay trabalham em paralelo e onde a tabela de conflito\ndo ",(0,r.jsx)(o.code,{children:"tools/eval/ARCH.md"})," manda."]}),"\n",(0,r.jsxs)(o.admonition,{title:"Elas ainda N\xc3O est\xe3o abertas no GitHub",type:"caution",children:[(0,r.jsxs)(o.p,{children:["Elas existem como arquivo, n\xe3o como issue. Existe um script pronto \u2014\n",(0,r.jsx)(o.code,{children:"docs/issues/abrir-issues.sh"}),", com ",(0,r.jsx)(o.a,{href:"https://cli.github.com/",children:(0,r.jsx)(o.code,{children:"gh"})})," autenticado:"]}),(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"bash docs/issues/abrir-issues.sh --dry-run # imprime t\xedtulo + labels, n\xe3o abre nada\nbash docs/issues/abrir-issues.sh --labels # cria as 8 labels usadas\nbash docs/issues/abrir-issues.sh # abre as 15\n"})}),(0,r.jsxs)(o.p,{children:["Ele \xe9 idempotente (procura issue com o mesmo t\xedtulo antes de criar) e ",(0,r.jsx)(o.strong,{children:"nunca foi\nexecutado"}),": o reposit\xf3rio \xe9 do dono e abrir issue \xe9 a\xe7\xe3o irrevers\xedvel com o nome dele.\nOu seja, se voc\xea procurar as tarefas na aba Issues, n\xe3o vai achar \u2014 leia os ",(0,r.jsx)(o.code,{children:".md"}),"."]})]}),"\n",(0,r.jsxs)(o.admonition,{title:"Esta lista j\xe1 teve cinco itens, e quatro foram feitos",type:"note",children:[(0,r.jsxs)(o.p,{children:["Ela mandava corrigir o ",(0,r.jsx)(o.code,{children:"README.md"})," (feito), adicionar ",(0,r.jsx)(o.code,{children:"arch"}),"/",(0,r.jsx)(o.code,{children:"arch:check"})," ao\n",(0,r.jsx)(o.code,{children:"package.json"})," (existem hoje), regenerar o ",(0,r.jsx)(o.code,{children:"ARCH.md"})," e fazer o ",(0,r.jsx)(o.code,{children:"tp-mount-probe"})," pular\nquando faltasse ",(0,r.jsx)(o.code,{children:"public/models/anims/"})," \u2014 pasta que ",(0,r.jsx)(o.strong,{children:"hoje est\xe1 versionada"})," (438 arquivos\nem ",(0,r.jsx)(o.code,{children:"git ls-files public/models/anims"}),"). Doc que manda fazer o que j\xe1 foi feito queima a\nprimeira contribui\xe7\xe3o de algu\xe9m; por isso a lista virou ponteiro para ",(0,r.jsx)(o.code,{children:"docs/issues/"}),",\nque \xe9 mantida."]}),(0,r.jsxs)(o.p,{children:["O \xfanico item da lista antiga que ",(0,r.jsx)(o.strong,{children:"continua valendo"})," \u2014 e agora est\xe1 consertado:\na mensagem das invariantes PX1\u2013PX4 apontava para ",(0,r.jsx)(o.code,{children:"tools/eval/motion.mjs"}),', que\nnunca existiu no git (ponteiro fantasma). Hoje as skips declaram honestamente\n"sem arn\xeas dedicado (d\xedvida PX)": o que existe de browser no CI \xe9 o\n',(0,r.jsx)(o.code,{children:"portao-browser"})," (boot real do jogo + grafite + silhueta da sele\xe7\xe3o), e um\narn\xeas de viewmodel dedicado continua sendo trabalho aberto."]})]}),"\n",(0,r.jsx)(o.h3,{id:"trabalho-de-verdade-ainda-acess\xedvel",children:"Trabalho de verdade, ainda acess\xedvel"}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"VM12 e VM1 nas armas espec\xedficas."})," VM12 falha em 5 de 52 medidas (pior ",(0,r.jsx)(o.code,{children:"famas"}),"@3:2\ncom 0,660 contra o teto 0,62); VM1 em 2 de 26 (",(0,r.jsx)(o.code,{children:"famas"}),", ",(0,r.jsx)(o.code,{children:"uzi"}),"). S\xe3o corre\xe7\xf5es por arma,\ncom faixa medida e ",(0,r.jsx)(o.code,{children:"vm-solve.mjs"})," dispon\xedvel para provar viabilidade. ",(0,r.jsx)(o.em,{children:"Frente:\nARMAS/VIEWMODEL."})]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"BOT8 \u2014 bot com linha de vis\xe3o e sem atirar."})," \xc9 a d\xedvida mais barata da lista, e a\ncausa raiz j\xe1 est\xe1 achada: ",(0,r.jsx)(o.code,{children:"game.js:5361"})," avalia ",(0,r.jsx)(o.code,{children:"const hasTurn = \u2026 this._duelToken(b)"}),"\n",(0,r.jsx)(o.strong,{children:"todo frame"}),', antes de qualquer gate de "pode atirar" \u2014 e ',(0,r.jsx)(o.code,{children:"_duelToken"})," n\xe3o consulta,\nele ",(0,r.jsx)(o.strong,{children:"reserva"})," o token. Bot recarregando ou sem linha de tiro rouba um dos 2 tokens e\nsegura; os outros atravessam o campo de vis\xe3o sem disparar. A corre\xe7\xe3o \xe9 mover a chamada\npara dentro do ",(0,r.jsx)(o.code,{children:"if"}),". Medido na \xfaltima execu\xe7\xe3o registrada: ",(0,r.jsx)(o.strong,{children:"4 epis\xf3dios, sil\xeancio\nm\xe1ximo 4,23 s"})," \u2014 e note que ",(0,r.jsx)(o.strong,{children:"piorou"})," desde os 2,7 / 3,03 s do baseline, o que faz\ndela tamb\xe9m um bom A/B. ",(0,r.jsx)(o.em,{children:"Frente: BOTS/JOGABILIDADE. Detalhe: BUG-03."})]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"Personagens: propor\xe7\xe3o (CHR1) e mapas de superf\xedcie."})," Cuidado com doc velha aqui: a\n",(0,r.jsx)(o.strong,{children:"CHR5B ficou VERDE"})," em 04/08 (os 27 de 44 personagens sem mapa de superf\xedcie foram a\n",(0,r.jsx)(o.strong,{children:"0 de 44"}),"), ent\xe3o esse item espec\xedfico ",(0,r.jsx)(o.strong,{children:"j\xe1 foi feito"})," \u2014 n\xe3o o refa\xe7a. O que segue\nvermelho \xe9 CHR1/CHR3/CHR4, e a causa de fundo \xe9 rig, n\xe3o runtime (BUG-10). Leia o\nKNOWN-BUGS antes de pegar. ",(0,r.jsx)(o.em,{children:"Frente: PERSONAGENS."})]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsxs)(o.strong,{children:[(0,r.jsx)(o.code,{children:"setTimeout"})," n\xe3o limpos no ",(0,r.jsx)(o.code,{children:"dispose()"})]})," \u2014 vazamento entre partidas, apontado em\n",(0,r.jsx)(o.code,{children:"RELATORIO-ANALISE.md:134"}),". ",(0,r.jsx)(o.strong,{children:"Os n\xfameros de linha daquele relat\xf3rio est\xe3o velhos"})," (o\n",(0,r.jsx)(o.code,{children:"game.js"})," andou ~1.000 linhas desde ent\xe3o); ache os atuais com\n",(0,r.jsx)(o.code,{children:"grep -n setTimeout public/js/game.js"})," e confira quais sobrevivem ao ",(0,r.jsx)(o.code,{children:"dispose()"}),". Bom\nPR de higiene com efeito med\xedvel no heap. ",(0,r.jsxs)(o.em,{children:["Frente: zona vermelha ",(0,r.jsx)(o.code,{children:"constructor"}),"/",(0,r.jsx)(o.code,{children:"update"}),"\n\u2014 coordene antes."]})]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(o.h3,{id:"alto-valor-precisa-de-conversa-antes",children:"Alto valor, precisa de conversa antes"}),"\n",(0,r.jsxs)(o.ol,{start:"5",children:["\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsxs)(o.strong,{children:["Extrair ",(0,r.jsx)(o.code,{children:"_updateBot()"})," (772 linhas)."]})," Marcado como candidato a extra\xe7\xe3o pelo\npr\xf3prio \xedndice gerado. Precisa de acordo pr\xe9vio sobre a parti\xe7\xe3o, porque a regi\xe3o \xe9\ndisputada."]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"Mapas como JSON (Fase 2)."})," Geometria, colliders, occluders, spawns, pickups e\nwaypoints em dado, com loader \xfanico e ",(0,r.jsx)(o.strong,{children:"waypoints validados por teste"}),'. \xc9 o que\ntransforma "PR de c\xf3digo arriscado" em "abre um JSON". Abra uma issue primeiro.']}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"Job de CI noturno com browser"})," para destravar PX1\u2013PX4. Quatro invariantes de pixel\nest\xe3o puladas desde sempre."]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(o.h2,{id:"processo",children:"Processo"}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:["Feature grande? ",(0,r.jsx)(o.strong,{children:"Abra uma issue antes"})," (veja ",(0,r.jsx)(o.code,{children:"IDEAS.md"}),")."]}),"\n",(0,r.jsxs)(o.li,{children:["Fork + branch ",(0,r.jsx)(o.strong,{children:(0,r.jsx)(o.code,{children:"v2/"})})," \u2014 ",(0,r.jsx)(o.code,{children:"v2/multiplayer"}),", ",(0,r.jsx)(o.code,{children:"v2/audio"}),", ",(0,r.jsx)(o.code,{children:"v2/ui-hud"}),". O prefixo\n\xe9 o ciclo de release (topo do ",(0,r.jsx)(o.code,{children:"CHANGELOG.md"}),"), e a conven\xe7\xe3o nasceu de um problema\nconcreto: em 04/08 a branch de trabalho ainda se chamava ",(0,r.jsx)(o.code,{children:"feat/evio-feel"})," \u2014 nome de uma\nfeature de julho \u2014 com ",(0,r.jsx)(o.strong,{children:"143 commits"})," de assuntos diferentes empilhados. Nome que n\xe3o\ndiz o que a branch \xe9 vira dep\xf3sito. (Fonte: ",(0,r.jsx)(o.code,{children:"CONTRIBUTING.md"}),".)"]}),"\n",(0,r.jsxs)(o.li,{children:["Rode ",(0,r.jsx)(o.code,{children:"npm run check"}),". Cole a sa\xedda no PR."]}),"\n",(0,r.jsxs)(o.li,{children:["PR pequeno, uma frente, descri\xe7\xe3o com n\xfameros e ",(0,r.jsx)(o.code,{children:"arquivo:linha"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Ao contribuir voc\xea licencia sob a licen\xe7a que o ",(0,r.jsx)(o.code,{children:"LICENSE"})," disser no momento do seu\nPR."]})," Qual \xe9 ela hoje e quais arquivos mudam junto numa troca: a se\xe7\xe3o de licen\xe7a do\n",(0,r.jsx)(o.code,{children:"CONTRIBUTING.md"}),". Se isso\nfor decisivo pra voc\xea, leia l\xe1 antes de escrever a primeira linha."]}),"\n"]}),"\n",(0,r.jsxs)(o.p,{children:["Reportando bug: o que aconteceu, o que esperava, passos pra reproduzir, navegador/SO e\nprint do console (F12). E se o bug for de comportamento, ele vai virar invariante \u2014 \xe9\nassim que ele nunca volta (",(0,r.jsx)(o.code,{children:"tools/eval/invariants.mjs:20-21"}),")."]})]})}function m(e={}){const{wrapper:o}={...(0,n.R)(),...e.components};return o?(0,r.jsx)(o,{...e,children:(0,r.jsx)(t,{...e})}):t(e)}},8453(e,o,a){a.d(o,{R:()=>d,x:()=>i});var s=a(6540);const r={},n=s.createContext(r);function d(e){const o=s.useContext(n);return s.useMemo(function(){return"function"==typeof e?e(o):{...o,...e}},[o,e])}function i(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),s.createElement(n.Provider,{value:o},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[514],{1757(e,o,a){a.r(o),a.d(o,{assets:()=>c,contentTitle:()=>i,default:()=>m,frontMatter:()=>d,metadata:()=>s,toc:()=>l});const s=JSON.parse('{"id":"colaborar","title":"Como colaborar","description":"Setup, como rodar o quality gate, o que um PR precisa, como adicionar arma / personagem / mapa, e as boas primeiras tarefas.","source":"@site/docs/colaborar.md","sourceDirName":".","slug":"/colaborar","permalink":"/docs/colaborar","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/colaborar.md","tags":[],"version":"current","sidebarPosition":6,"frontMatter":{"id":"colaborar","title":"Como colaborar","sidebar_label":"Como colaborar","sidebar_position":6,"description":"Setup, como rodar o quality gate, o que um PR precisa, como adicionar arma / personagem / mapa, e as boas primeiras tarefas."},"sidebar":"dev","previous":{"title":"Arquitetura","permalink":"/docs/arquitetura"},"next":{"title":"Estado atual","permalink":"/docs/estado"}}');var r=a(4848),n=a(8453);const d={id:"colaborar",title:"Como colaborar",sidebar_label:"Como colaborar",sidebar_position:6,description:"Setup, como rodar o quality gate, o que um PR precisa, como adicionar arma / personagem / mapa, e as boas primeiras tarefas."},i="Como colaborar",c={},l=[{value:"Setup",id:"setup",level:2},{value:"Rodar o quality gate",id:"rodar-o-quality-gate",level:2},{value:"Antes de dizer que consertou: mute",id:"antes-de-dizer-que-consertou-mute",level:3},{value:"Se o seu PR \xe9 um conserto de bug",id:"se-o-seu-pr-\xe9-um-conserto-de-bug",level:3},{value:"O que um PR precisa",id:"o-que-um-pr-precisa",level:2},{value:"1. Uma invariante nova \u2014 ou a raz\xe3o de n\xe3o precisar",id:"1-uma-invariante-nova--ou-a-raz\xe3o-de-n\xe3o-precisar",level:3},{value:"2. O quality gate n\xe3o pode piorar",id:"2-o-quality-gate-n\xe3o-pode-piorar",level:3},{value:"3. N\xfameros, com arquivo:linha",id:"3-n\xfameros-com-arquivolinha",level:3},{value:"4. Uma frente por PR",id:"4-uma-frente-por-pr",level:3},{value:"5. Higiene do reposit\xf3rio",id:"5-higiene-do-reposit\xf3rio",level:3},{value:"6. Linha editorial",id:"6-linha-editorial",level:3},{value:"Como adicionar uma arma",id:"como-adicionar-uma-arma",level:2},{value:"Como adicionar um personagem",id:"como-adicionar-um-personagem",level:2},{value:"Como adicionar um mapa",id:"como-adicionar-um-mapa",level:2},{value:"Boas primeiras tarefas",id:"boas-primeiras-tarefas",level:2},{value:"Muito boas para o primeiro PR",id:"muito-boas-para-o-primeiro-pr",level:3},{value:"Trabalho de verdade, ainda acess\xedvel",id:"trabalho-de-verdade-ainda-acess\xedvel",level:3},{value:"Alto valor, precisa de conversa antes",id:"alto-valor-precisa-de-conversa-antes",level:3},{value:"Processo",id:"processo",level:2}];function t(e){const o={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,n.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(o.header,{children:(0,r.jsx)(o.h1,{id:"como-colaborar",children:"Como colaborar"})}),"\n",(0,r.jsxs)(o.p,{children:["O n\xfamero abaixo n\xe3o \xe9 ret\xf3rica, e n\xe3o \xe9 escrito \xe0 m\xe3o: sai de ",(0,r.jsx)(o.code,{children:"git shortlog -sn --no-merges"})," descontando os autores que s\xe3o agentes de IA (que assinam como ",(0,r.jsx)(o.code,{children:"Claude"})," /\n",(0,r.jsx)(o.code,{children:"Claude (gauntlet \u2026)"}),")."]}),"\n","\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"11 identidades de autoria humana"})," assinam commit no hist\xf3rico ",(0,r.jsx)(o.strong,{children:"desta branch"}),": ",(0,r.jsx)(o.code,{children:"ruben-cytonic"}),", ",(0,r.jsx)(o.code,{children:"Ruben"}),", ",(0,r.jsx)(o.code,{children:"Emerson Garrido"}),", ",(0,r.jsx)(o.code,{children:"rubenmarcus"}),", ",(0,r.jsx)(o.code,{children:"Ruben Marcus"}),", ",(0,r.jsx)(o.code,{children:"William Oliveira"}),", ",(0,r.jsx)(o.code,{children:"Juan Versolato Lopes"}),", ",(0,r.jsx)(o.code,{children:"daeeseD"}),", ",(0,r.jsx)(o.code,{children:"matheusgb"}),", ",(0,r.jsx)(o.code,{children:"Man\xe1 Soares"}),", ",(0,r.jsx)(o.code,{children:"daltonfontes"}),". O resto dos commits \xe9 assinado por agentes de IA. Branch n\xe3o \xe9 reposit\xf3rio: quem contribuiu num ramo que esta branch n\xe3o cont\xe9m ",(0,r.jsx)(o.strong,{children:"n\xe3o aparece aqui"}),"."]}),"\n",(0,r.jsxs)(o.blockquote,{children:["\n",(0,r.jsxs)(o.p,{children:["Bloco gerado por ",(0,r.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,r.jsx)(o.code,{children:"git shortlog -sn --no-merges (descontando autores que s\xe3o agentes)"})]}),"\n"]}),"\n","\n",(0,r.jsx)(o.p,{children:"N\xe3o existe time, n\xe3o existe comunidade, n\xe3o existe fila de revisores \u2014 existem essas\npessoas e um quality gate automatizado."}),"\n",(0,r.jsx)(o.admonition,{title:"O bloco acima conta a BRANCH, e o projeto \xe9 maior que ela",type:"note",children:(0,r.jsxs)(o.p,{children:["A ",(0,r.jsx)(o.code,{children:"main"})," tem um quarto contribuidor que esta branch de trabalho n\xe3o cont\xe9m \u2014 13 commits\nde um cliente desktop, mesclados em julho. Quem, quanto e por que isso importa para\nqualquer decis\xe3o de licen\xe7a est\xe1 no ",(0,r.jsx)(o.code,{children:"CONTRIBUTING.md"})," (se\xe7\xe3o de licen\xe7a e superf\xedcies)."]})}),"\n",(0,r.jsxs)(o.p,{children:["Isso \xe9 relevante pra voc\xea de duas formas opostas. A ruim: se o seu PR travar, pode\ndemorar. A boa: ",(0,r.jsx)(o.strong,{children:"quase toda a r\xe9gua \xe9 m\xe1quina."})," ",(0,r.jsx)(o.code,{children:"npm run check:fast"})," te d\xe1 o mesmo veredito\nque o mantenedor daria, antes de voc\xea abrir o PR, sem esperar ningu\xe9m. A barreira \xe9 baixa\n",(0,r.jsx)(o.strong,{children:"de prop\xf3sito"})," \u2014 \xe9 um dos princ\xedpios que n\xe3o mudam do\n",(0,r.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,r.jsx)(o.code,{children:"docs/ROADMAP.md"})}),".\nMas a r\xe9gua n\xe3o \xe9."]}),"\n",(0,r.jsxs)(o.p,{children:["Resumo em uma frase: ",(0,r.jsx)(o.strong,{children:"traga o n\xfamero."})," Um PR que muda comportamento vis\xedvel e n\xe3o traz\nnem uma invariante nova nem a raz\xe3o de n\xe3o precisar de uma vai voltar com uma pergunta."]}),"\n",(0,r.jsx)(o.h2,{id:"setup",children:"Setup"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"git clone https://github.com/rubenmarcus/csbrasil.git && cd csbrasil\nnpm install\nnpm run dev # http://localhost:4321 \u2014 a rota raiz J\xc1 \xc9 o jogo\n"})}),"\n",(0,r.jsx)(o.p,{children:"Opcional:"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"npm run fetch-audio # pacote de \xe1udio (sem ele: sons sintetizados)\n"})}),"\n",(0,r.jsxs)(o.p,{children:["Requisitos: Node 22 (\xe9 o que o CI usa, ",(0,r.jsx)(o.code,{children:".github/workflows/ci.yml:14"}),") e Python 3 para\nparte do arn\xeas (",(0,r.jsx)(o.code,{children:"ref-measure.py"}),", ",(0,r.jsx)(o.code,{children:"char_probe.py"}),", ",(0,r.jsx)(o.code,{children:"mat_shade.py"})," \u2014 usam numpy e PIL)."]}),"\n",(0,r.jsxs)(o.admonition,{type:"caution",children:[(0,r.jsxs)(o.mdxAdmonitionTitle,{children:["Servir ",(0,r.jsx)(o.code,{children:"public/"})," N\xc3O roda o jogo"]}),(0,r.jsxs)(o.p,{children:["N\xe3o existe ",(0,r.jsx)(o.code,{children:"public/index.html"}),": o HTML do jogo \xe9 ",(0,r.jsx)(o.code,{children:"src/pages/index.astro"}),", na rota raiz.\nUse ",(0,r.jsx)(o.code,{children:"npm run dev"}),". Detalhes e prova em\n",(0,r.jsx)(o.a,{href:"/docs/#a-pegadinha-que-custa-a-primeira-hora-de-todo-mundo",children:"Come\xe7ando"}),"."]})]}),"\n",(0,r.jsx)(o.h2,{id:"rodar-o-quality-gate",children:"Rodar o quality gate"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"npm run eval:vm # OBRIGAT\xd3RIO ANTES \u2014 ver o aviso abaixo\nnode tools/eval/invariants.mjs # o quality gate inteiro\nnode tools/eval/invariants.mjs --json # sa\xedda pra m\xe1quina\nnpm run check:fast # syntax + quality gate (r\xe9guas de node puro)\n"})}),"\n",(0,r.jsxs)(o.admonition,{type:"danger",children:[(0,r.jsxs)(o.mdxAdmonitionTitle,{children:[(0,r.jsx)(o.code,{children:"eval:vm"})," roda ANTES de ",(0,r.jsx)(o.code,{children:"invariants.mjs"}),". Sempre."]}),(0,r.jsxs)(o.p,{children:["As invariantes de viewmodel (VM1\u2013VM19) ",(0,r.jsx)(o.strong,{children:"leem"})," ",(0,r.jsx)(o.code,{children:"tools/eval/vm_mint_audit.json"}),", que \xe9 o\n",(0,r.jsx)(o.code,{children:"eval:vm"})," quem ",(0,r.jsx)(o.strong,{children:"escreve"}),". Rodar as invariantes com esse JSON velho mede o viewmodel de\nontem e ",(0,r.jsx)(o.strong,{children:"inventa vermelha"}),": em 04/08/2026 o JSON estava em ",(0,r.jsx)(o.code,{children:"V0=80\xb0"})," contra o ",(0,r.jsx)(o.code,{children:"game.js"}),"\nem ",(0,r.jsx)(o.code,{children:"V0=42\xb0"}),", e a VM5 acusava ",(0,r.jsx)(o.strong,{children:"26/26 armas fora"}),"; depois de ",(0,r.jsx)(o.code,{children:"npm run eval:vm"}),", ",(0,r.jsx)(o.strong,{children:"3/26"}),".\nA VM1 caiu de 26/26 para 2/26 e a VM9 ficou verde."]}),(0,r.jsxs)(o.p,{children:["A ordem do ",(0,r.jsx)(o.code,{children:"npm run check"})," j\xe1 foi corrigida (",(0,r.jsx)(o.code,{children:"package.json"}),") \u2014 o cuidado \xe9 para quando\nvoc\xea chamar ",(0,r.jsx)(o.code,{children:"node tools/eval/invariants.mjs"})," na m\xe3o. Detalhe: BUG-02 do\n",(0,r.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/KNOWN-BUGS.md",children:(0,r.jsx)(o.code,{children:"KNOWN-BUGS.md"})}),"."]})]}),"\n",(0,r.jsxs)(o.p,{children:["Custo real: numa m\xe1quina de 2 CPUs, ",(0,r.jsx)(o.strong,{children:"cerca de 10 minutos"}),". Ele sobe o jogo real cinco\nvezes (uma por mapa), roda 60 s de simula\xe7\xe3o de bot por mapa e audita todos os GLBs de arma.\nRode antes de abrir o PR, n\xe3o depois de receber a review."]}),"\n",(0,r.jsx)(o.p,{children:"Arn\xeases individuais, quando voc\xea quiser iterar r\xe1pido numa frente s\xf3:"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"node tools/eval/vm-mint-audit.mjs # enquadramento de viewmodel (todo o arsenal)\nnode tools/eval/vm-solve.mjs # existe ponto vi\xe1vel pras invariantes de VM?\nnode tools/eval/vm-solve.mjs --atual # s\xf3 as margens da config atual (instant\xe2neo)\nnode tools/eval/botsim.mjs 60 all # navega\xe7\xe3o de bots, todos os mapas, sementes fixas\nnode tools/eval/char-probe.mjs # personagens (C1..C6)\nnode tools/eval/map-check.mjs all # geometria de mapa (MAP1-MAP3, CTF1)\nnode tools/eval/mat-check.mjs # material/luz/fog/textura\nnode tools/eval/pickup-check.mjs # todo pickup \xe9 alcan\xe7\xe1vel?\nnode tools/eval/ui-check.mjs # UI1 contraste \xb7 UI2 polui\xe7\xe3o \xb7 UI3 \xe1rea morta \xb7 UI4 ritmo\n"})}),"\n",(0,r.jsx)(o.h3,{id:"antes-de-dizer-que-consertou-mute",children:"Antes de dizer que consertou: mute"}),"\n",(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"MUT=ui1_ctf_scrim_fraco node tools/eval/ui-check.mjs # espera UI1 VERMELHA\n"})}),"\n",(0,r.jsxs)(o.p,{children:["Desfa\xe7a a sua pr\xf3pria corre\xe7\xe3o e confira que o quality gate ",(0,r.jsx)(o.strong,{children:"fica vermelho"}),". Se ficar verde,\no que voc\xea mediu n\xe3o \xe9 o que voc\xea consertou. \xc9 a li\xe7\xe3o mais cara deste reposit\xf3rio e ela\ntem uma p\xe1gina inteira: ",(0,r.jsx)(o.a,{href:"/docs/quality-gates#teste-de-muta%C3%A7%C3%A3o-da-pr%C3%B3pria-r%C3%A9gua",children:"Teste de muta\xe7\xe3o"}),"."]}),"\n",(0,r.jsx)(o.h3,{id:"se-o-seu-pr-\xe9-um-conserto-de-bug",children:"Se o seu PR \xe9 um conserto de bug"}),"\n",(0,r.jsxs)(o.p,{children:["Use a skill ",(0,r.jsx)(o.code,{children:"bug-hunt"})," (",(0,r.jsx)(o.code,{children:".claude/skills/bug-hunt/SKILL.md"}),"). Ela \xe9 o passo a passo desta\ndoutrina aplicado a defeito \u2014 com o caso real que comprou cada regra, o gabarito da entrada\ndo ",(0,r.jsx)(o.code,{children:"KNOWN-BUGS.md"})," e o do relat\xf3rio final, incluindo como declarar o que voc\xea ",(0,r.jsx)(o.strong,{children:"n\xe3o"}),"\nverificou. Serve para agente e para gente."]}),"\n",(0,r.jsx)(o.h2,{id:"o-que-um-pr-precisa",children:"O que um PR precisa"}),"\n",(0,r.jsx)(o.h3,{id:"1-uma-invariante-nova--ou-a-raz\xe3o-de-n\xe3o-precisar",children:"1. Uma invariante nova \u2014 ou a raz\xe3o de n\xe3o precisar"}),"\n",(0,r.jsxs)(o.p,{children:["Esta \xe9 a regra que define o projeto. Todo PR que muda ",(0,r.jsx)(o.strong,{children:"comportamento observ\xe1vel"})," traz\numa das duas coisas:"]}),"\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Uma invariante nova"})," em ",(0,r.jsx)(o.code,{children:"tools/eval/invariants.mjs"}),", com teto que tem proced\xeancia\n(arquivo de refer\xeancia + pixel medido + script que reproduz), ou"]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Uma frase na descri\xe7\xe3o do PR"}),' dizendo por que n\xe3o precisa. Raz\xf5es v\xe1lidas: "j\xe1 \xe9\ncoberto pela invariante X" (diga qual), "\xe9 refatora\xe7\xe3o sem mudan\xe7a observ\xe1vel \u2014 o\nquality gate d\xe1 o mesmo placar antes e depois" (cole os dois), "\xe9 conte\xfado puro (texto,\nasset) sem regra de jogo associada".']}),"\n"]}),"\n",(0,r.jsx)(o.p,{children:'Raz\xe3o inv\xe1lida: "testei manualmente e ficou bom".'}),"\n",(0,r.jsxs)(o.p,{children:["Por qu\xea: ",(0,r.jsx)(o.strong,{children:"inten\xe7\xe3o que n\xe3o vira invariante \xe9 otimizada para fora"}),". Uma rodada levou o\nquality gate de 16/21 para 19/21 sem afrouxar um teto sequer, e foi reprovada, porque destruiu\nem sil\xeancio uma decis\xe3o est\xe9tica que nenhuma invariante codificava. Caso completo em\n",(0,r.jsx)(o.a,{href:"/docs/quality-gates#lei-1--inten%C3%A7%C3%A3o-que-n%C3%A3o-vira-invariante-%C3%A9-otimizada-para-fora",children:"O quality gate"}),"."]}),"\n",(0,r.jsx)(o.h3,{id:"2-o-quality-gate-n\xe3o-pode-piorar",children:"2. O quality gate n\xe3o pode piorar"}),"\n",(0,r.jsxs)(o.p,{children:["Cole a sa\xedda de ",(0,r.jsx)(o.code,{children:"node tools/eval/invariants.mjs"})," antes e depois. Se alguma cr\xedtica ficou\nvermelha, o PR n\xe3o entra. Se voc\xea ",(0,r.jsx)(o.strong,{children:"consertou"})," uma vermelha, diga qual e mostre."]}),"\n",(0,r.jsxs)(o.p,{children:["O estado do quality gate deve ser medido (",(0,r.jsx)(o.a,{href:"/docs/estado",children:"veja como"}),"); a lista viva com causa raiz est\xe1\nno ",(0,r.jsx)(o.code,{children:"KNOWN-BUGS.md"}),"). Isso n\xe3o \xe9 licen\xe7a para piorar: o compromisso \xe9 ",(0,r.jsx)(o.em,{children:'"a sua mudan\xe7a n\xe3o\nacrescenta vermelho"'}),"."]}),"\n",(0,r.jsxs)(o.h3,{id:"3-n\xfameros-com-arquivolinha",children:["3. N\xfameros, com ",(0,r.jsx)(o.code,{children:"arquivo:linha"})]}),"\n",(0,r.jsxs)(o.p,{children:['A afirma\xe7\xe3o "melhorei a ilumina\xe7\xe3o" n\xe3o \xe9 revis\xe1vel. "O ch\xe3o do ',(0,r.jsx)(o.code,{children:"praca_poderes"})," estava 8 pontos\nde L* acima das paredes, causa em ",(0,r.jsx)(o.code,{children:"map_brasilia.js:NNN"}),', corrigido para X" \xe9. Essa\nexig\xeancia n\xe3o \xe9 estilo \u2014 \xe9 o que permite que a pr\xf3xima rodada confira o seu trabalho.']}),"\n",(0,r.jsx)(o.h3,{id:"4-uma-frente-por-pr",children:"4. Uma frente por PR"}),"\n",(0,r.jsxs)(o.p,{children:["Consulte a tabela de conflito (",(0,r.jsx)(o.a,{href:"/docs/arquitetura#a-tabela-de-conflito",children:"Arquitetura"}),"). Um\nPR que toca armas + UI + mapa \xe9 tr\xeas PRs escondidos, e vai colidir com tr\xeas frentes. Em\n",(0,r.jsx)(o.code,{children:"game.js"}),", edite por trecho; nunca sobrescreva o arquivo inteiro."]}),"\n",(0,r.jsx)(o.h3,{id:"5-higiene-do-reposit\xf3rio",children:"5. Higiene do reposit\xf3rio"}),"\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"node --check"})," em cada arquivo de ",(0,r.jsx)(o.code,{children:"public/js/"})," que voc\xea editou (o CI faz isso primeiro,\n",(0,r.jsx)(o.code,{children:".github/workflows/ci.yml:19-20"}),")."]}),"\n",(0,r.jsxs)(o.li,{children:["Coment\xe1rio ",(0,r.jsx)(o.strong,{children:"em portugu\xeas explicando o porqu\xea"}),", n\xe3o o qu\xea. \xc9 a cultura do repo e \xe9 o\nque sobrevive ao pr\xf3ximo handoff."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Nunca delete coment\xe1rio de proced\xeancia"})," num PR de limpeza. Aquele par\xe1grafo longo\nexplicando de onde veio o n\xfamero 0,513 \xe9 o que impede a pr\xf3xima rodada de repetir tr\xeas\ndias perdidos."]}),"\n",(0,r.jsxs)(o.li,{children:["Mexeu em ",(0,r.jsx)(o.code,{children:"public/js/*.js"}),"? ",(0,r.jsxs)(o.strong,{children:["Bump o ",(0,r.jsx)(o.code,{children:"?v="})," nos dois lados"]})," \u2014 ",(0,r.jsx)(o.code,{children:"public/js/version.js"})," e o\nimport map de ",(0,r.jsx)(o.code,{children:"src/pages/index.astro"}),'. J\xe1 custou dias de "corre\xe7\xe3o que n\xe3o chegava".']}),"\n",(0,r.jsxs)(o.li,{children:["Mexeu em ",(0,r.jsx)(o.code,{children:"public/js/*.js"}),", no ",(0,r.jsx)(o.code,{children:"maps.js"}),", no ",(0,r.jsx)(o.code,{children:"characters.js"})," ou numa depend\xeancia?\n",(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"npm run docs"})]})," e commite junto. O ",(0,r.jsx)(o.code,{children:"docs:check"})," est\xe1 no ",(0,r.jsx)(o.code,{children:"check:fast"})," e vai\nreprovar \u2014 leva menos de um segundo e \xe9 o que impede a doc de voltar a mentir."]}),"\n",(0,r.jsxs)(o.li,{children:["Nada de asset com copyright. Nada de ",(0,r.jsx)(o.code,{children:"service_role"})," key commitada."]}),"\n",(0,r.jsx)(o.li,{children:"Nada de depend\xeancia de runtime no jogo. Three.js \xe9 vendorizado; o jogo tem que rodar\narrastando a pasta pra um host est\xe1tico."}),"\n"]}),"\n",(0,r.jsx)(o.h3,{id:"6-linha-editorial",children:"6. Linha editorial"}),"\n",(0,r.jsxs)(o.p,{children:["De ",(0,r.jsx)(o.code,{children:"CONTRIBUTING.md:7-16"}),": o jogo n\xe3o tem lado pol\xedtico (os dois times t\xeam a mesma\nmec\xe2nica), n\xe3o incita \xf3dio, n\xe3o usa pessoas reais \u2014 s\xf3 arqu\xe9tipos originais, sem gore.\nContribui\xe7\xf5es que violem isso s\xe3o recusadas. N\xe3o \xe9 burocracia: \xe9 o que protege o projeto\nde takedown e de virar outra coisa."]}),"\n",(0,r.jsx)(o.h2,{id:"como-adicionar-uma-arma",children:"Como adicionar uma arma"}),"\n",(0,r.jsxs)(o.p,{children:["O pipeline \xe9 data-driven a partir do GLB. Os GLBs de arma vivem em ",(0,r.jsx)(o.code,{children:"public/models/weapons/"}),"\n(a contagem est\xe1 no bloco gerado de ",(0,r.jsx)(o.a,{href:"/docs/",children:"Come\xe7ando"}),")."]}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Coloque o GLB"})," em ",(0,r.jsx)(o.code,{children:"public/models/weapons/.glb"}),". S\xf3 geometria \u2014 o material vem\ndo pipeline (",(0,r.jsx)(o.code,{children:"MAT1"})," exige ",(0,r.jsx)(o.code,{children:"metallicFactor 1 / roughnessFactor 1"})," com mapa\nmetallicRoughness, que \xe9 o padr\xe3o de todas as atuais)."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Declare a arma"})," em ",(0,r.jsx)(o.code,{children:"public/js/weapons.js"}),". Os campos que o quality gate l\xea:","\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"len"})," \u2014 comprimento em metros. ",(0,r.jsxs)(o.strong,{children:[(0,r.jsx)(o.code,{children:"ARM4"})," reprova acima de 1,25 m"]})," fora de sniper de\nferrolho. \xc9 o campo que normaliza a escala; n\xe3o \xe9 decora\xe7\xe3o."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"gripZ"})," \u2014 fra\xe7\xe3o do comprimento, contada ",(0,r.jsx)(o.strong,{children:"a partir da boca"}),", onde fica o grip\n(ak/m4 usam 0,62 \u2014 cai no guarda-mato). \xc9 o que ancora a m\xe3o."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"vm"})," \u2014 multiplicador de escala do mesh no viewmodel. Existe porque a ",(0,r.jsx)(o.code,{children:"m92"})," batia\n14,50% contra o teto medido de 13,09% da VM18b."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"scope: true"})," ",(0,r.jsx)(o.strong,{children:"exige"})," ",(0,r.jsx)(o.code,{children:"spreadScope"})," declarado \u2014 \xe9 a ",(0,r.jsx)(o.code,{children:"ARM1"}),', e ela existe por causa\ndo "sniper sem zoom".']}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Rode o auditor:"})," ",(0,r.jsx)(o.code,{children:"node tools/eval/vm-mint-audit.mjs"}),". Ele abre o GLB com parser\npr\xf3prio, projeta o viewmodel nos dois aspectos e escreve ",(0,r.jsx)(o.code,{children:"tools/eval/vm_mint_audit.json"}),".\n",(0,r.jsx)(o.strong,{children:"Esse JSON \xe9 versionado"})," \u2014 sem ele, VM1\u2013VM6/VM9/VM10 viram PULADAS, que \xe9 quality gate\nverde por aus\xeancia de dado (",(0,r.jsx)(o.code,{children:".github/workflows/ci.yml:24-27"}),")."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Rode o quality gate."})," Voc\xea vai enfrentar VM1, VM3, VM5, VM9, VM12, VM16, VM18, VM18b,\nVM19 \u2014 nove invariantes de enquadramento, todas com faixa medida em frame de\nrefer\xeancia. Se n\xe3o fechar, use ",(0,r.jsx)(o.code,{children:"node tools/eval/vm-solve.mjs"})," em vez de tunar no olho:\nele l\xea os tetos do pr\xf3prio ",(0,r.jsx)(o.code,{children:"invariants.mjs"})," e diz se existe ponto vi\xe1vel, ou ",(0,r.jsx)(o.strong,{children:"qual par\nde invariantes se cruza vazio e por quanto"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Commite o ",(0,r.jsx)(o.code,{children:"vm_mint_audit.json"})," atualizado"]})," junto com o resto."]}),"\n"]}),"\n",(0,r.jsx)(o.h2,{id:"como-adicionar-um-personagem",children:"Como adicionar um personagem"}),"\n",(0,r.jsxs)(o.p,{children:["45 GLBs em ",(0,r.jsx)(o.code,{children:"public/models/characters/"}),", 44 medidos pelo ",(0,r.jsx)(o.code,{children:"char-probe.mjs"}),"."]}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"GLB com rig"}),", na bind pose, p\xe9s no ch\xe3o. ",(0,r.jsx)(o.code,{children:"CHR3"})," exige ",(0,r.jsx)(o.code,{children:"|base da bbox| \u2264 0,01 m"})," na\nbind pose ",(0,r.jsx)(o.strong,{children:"e em cada clipe"})," \u2014 o sinal separa dois defeitos: ",(0,r.jsx)(o.code,{children:"y < 0"})," \xe9 p\xe9 dentro do\nch\xe3o, ",(0,r.jsx)(o.code,{children:"y > 0"})," \xe9 boneco no ar."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.strong,{children:"Declare"})," em ",(0,r.jsx)(o.code,{children:"public/js/characters.js"})," / ",(0,r.jsx)(o.code,{children:"public/js/glbchars.js"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"node tools/eval/char-probe.mjs"}),"."]})," O que ele vai cobrar:","\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR1"}),' \u2014 propor\xe7\xe3o antropom\xe9trica e \xedndice de "bal\xe3o". Hoje ',(0,r.jsx)(o.strong,{children:"est\xe1 vermelha para o\nelenco inteiro"}),", ent\xe3o n\xe3o \xe9 voc\xea que a quebrou; mas n\xe3o a piore."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR2"})," \u2014 altura do corpo dentro de meia hitbox de cabe\xe7a (dispers\xe3o \u2264 0,15 m). Medida\n",(0,r.jsx)(o.strong,{children:"sem adere\xe7o"}),": chap\xe9u/cabelo/mastro inflam a bbox e fazem o caminho GLB (a evid\xeancia da pr\xf3pria CHR2 aponta ",(0,r.jsx)(o.code,{children:"glbchars.js:319-322"}),")\nencolher o corpo."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR4"})," \u2014 nenhuma palma nasce enterrada no corpo."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR5"}),"/",(0,r.jsx)(o.code,{children:"CHR5B"})," \u2014 acabamento (normal + roughness + AO). A CHR5B ",(0,r.jsx)(o.strong,{children:"ficou verde em\n04/08"}),': era o "tr\xeas n\xedveis de acabamento na mesma tela" que o dono descreveu, com\nboa parte do elenco sem nenhum mapa de superf\xedcie, e hoje \xe9 zero personagem sem.\nPersonagem novo ',(0,r.jsx)(o.strong,{children:"sem"})," normal + roughness reabre a vermelha \u2014 traga os mapas."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CHR6"})," \u2014 nenhum par com a mesma silhueta (IoU \u2264 0,98)."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(o.h2,{id:"como-adicionar-um-mapa",children:"Como adicionar um mapa"}),"\n",(0,r.jsxs)(o.p,{children:["Hoje mapas s\xe3o ",(0,r.jsx)(o.strong,{children:"c\xf3digo"}),", n\xe3o dado: cada ",(0,r.jsx)(o.code,{children:"map_*.js"})," \xe9 geometria declarada \xe0 m\xe3o, e os\nmaiores rivalizam em tamanho com os m\xf3dulos de sistema. Migrar isso para JSON \xe9 a Fase 2\nconte\xfado como dado do\n",(0,r.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,r.jsx)(o.code,{children:"docs/ROADMAP.md"})}),", e \xe9 a\ncontribui\xe7\xe3o de maior alavancagem do projeto."]}),"\n",(0,r.jsxs)(o.p,{children:["O registro, gerado do ",(0,r.jsx)(o.code,{children:"MAPS"})," de ",(0,r.jsx)(o.code,{children:"public/js/maps.js"}),":"]}),"\n","\n",(0,r.jsxs)(o.table,{children:[(0,r.jsx)(o.thead,{children:(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.th,{children:"Id"}),(0,r.jsx)(o.th,{children:"Nome no menu"}),(0,r.jsx)(o.th,{children:"Abre em"}),(0,r.jsxs)(o.th,{children:["Arquivo em ",(0,r.jsx)(o.code,{children:"public/js/"})]}),(0,r.jsx)(o.th,{style:{textAlign:"right"},children:"Linhas"})]})}),(0,r.jsxs)(o.tbody,{children:[(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"praca_poderes"})}),(0,r.jsx)(o.td,{children:"Pra\xe7a dos Tr\xeas Poderes"}),(0,r.jsx)(o.td,{children:"rodadas"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_brasilia.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"1.830"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"piscina_treta"})}),(0,r.jsx)(o.td,{children:"Piscina da Treta"}),(0,r.jsx)(o.td,{children:"rodadas"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_piscina.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"810"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"loja_h"})}),(0,r.jsx)(o.td,{children:"Loja H (Estacionamento)"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_havan.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"1.964"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"ferro_velho"})}),(0,r.jsx)(o.td,{children:"Ferro Velho do Z\xe9"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_ferrovelho.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"1.888"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"quebrada"})}),(0,r.jsx)(o.td,{children:"Quebrada (Rua do Baile)"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_quebrada.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"1.599"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"posto_treta"})}),(0,r.jsx)(o.td,{children:"Posto da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_posto.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"489"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"upa_24h"})}),(0,r.jsx)(o.td,{children:"UPA 24h da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_upa.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"288"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"obras_prefeitura"})}),(0,r.jsx)(o.td,{children:"Obras da Prefeitura"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_obras.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"240"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"atacadao_treta"})}),(0,r.jsx)(o.td,{children:"Atacad\xe3o da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_atacadao.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"255"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"parque_treta"})}),(0,r.jsx)(o.td,{children:"Parque da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_parque.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"402"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"velho_oeste"})}),(0,r.jsx)(o.td,{children:"Velho Oeste da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_velho_oeste.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"433"})]}),(0,r.jsxs)(o.tr,{children:[(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"penitenciaria"})}),(0,r.jsx)(o.td,{children:"Penitenci\xe1ria da Treta"}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.strong,{children:"captura"})}),(0,r.jsx)(o.td,{children:(0,r.jsx)(o.code,{children:"map_penitenciaria.js"})}),(0,r.jsx)(o.td,{style:{textAlign:"right"},children:"247"})]})]})]}),"\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"12 mapas registrados"})," \u2014 2 abrem em rodadas e 10 em captura. ",(0,r.jsx)(o.code,{children:"ctfMode"})," ",(0,r.jsx)(o.strong,{children:"abre"})," o mapa em captura, n\xe3o prende: o jogador troca no menu (\xe9 a ",(0,r.jsx)(o.code,{children:"MOD1"}),"). H\xe1 14 arquivos ",(0,r.jsx)(o.code,{children:"map_*.js"})," em ",(0,r.jsx)(o.code,{children:"public/js/"})," \u2014 arquivo no disco ",(0,r.jsx)(o.strong,{children:"n\xe3o"})," implica mapa jog\xe1vel."]}),"\n",(0,r.jsxs)(o.blockquote,{children:["\n",(0,r.jsxs)(o.p,{children:["Bloco gerado por ",(0,r.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,r.jsx)(o.code,{children:"objeto MAPS de public/js/maps.js"})]}),"\n"]}),"\n","\n",(0,r.jsx)(o.p,{children:"Dois avisos que custam tempo se voc\xea n\xe3o souber:"}),"\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:[(0,r.jsx)(o.code,{children:"praca_old"}),' ("Pra\xe7a (cl\xe1ssico)") N\xc3O existe mais.']})," Saiu do registro e o\n",(0,r.jsx)(o.code,{children:"public/js/map.js"})," foi apagado junto (pedido literal do dono: ",(0,r.jsx)(o.em,{children:'"vamos apagar pra\xe7a\ncl\xe1ssica"'}),"). Se voc\xea encontrar ",(0,r.jsx)(o.code,{children:"praca_old"})," numa sa\xedda de r\xe9gua, essa sa\xedda \xe9 anterior \xe0\nremo\xe7\xe3o \u2014 \xe9 o caso do hist\xf3rico explicado em ",(0,r.jsx)(o.a,{href:"/docs/estado",children:"Estado atual"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:[(0,r.jsx)(o.code,{children:"map_piscinao_ramos.js"})," existe no disco e N\xc3O est\xe1 no registro"]}),' (\xe9 a vers\xe3o "Piscin\xe3o",\nfora do menu). Arquivo de mapa em ',(0,r.jsx)(o.code,{children:"public/js/"})," n\xe3o implica mapa jog\xe1vel; quem decide \xe9\no objeto ",(0,r.jsx)(o.code,{children:"MAPS"}),"."]}),"\n"]}),"\n",(0,r.jsx)(o.p,{children:"Para adicionar um mapa no formato de hoje:"}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Crie ",(0,r.jsx)(o.code,{children:"public/js/map_.js"})]})," exportando uma fun\xe7\xe3o ",(0,r.jsx)(o.code,{children:"build()"}),". Use\n",(0,r.jsx)(o.code,{children:"map_piscina.js"})," como refer\xeancia \u2014 \xe9 o menor dos registrados (a tabela acima traz o\ntamanho de cada um)."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Registre em ",(0,r.jsx)(o.code,{children:"public/js/maps.js:8-36"})]})," \u2014 nome exibido, ",(0,r.jsx)(o.code,{children:"build"}),", e ",(0,r.jsx)(o.code,{children:"ctfMode: true"})," se\na geometria foi desenhada em volta de bandeiras. ",(0,r.jsx)(o.code,{children:"ctfMode"})," ",(0,r.jsx)(o.strong,{children:"abre"})," o mapa em captura;\nn\xe3o prende. ",(0,r.jsx)(o.strong,{children:"N\xe3o"})," existe mais ",(0,r.jsx)(o.code,{children:"ctfOnly"}),": ",(0,r.jsx)(o.code,{children:"MOD1"})," reprova qualquer mapa que force o\nmodo. O jogador escolhe."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"node tools/eval/map-check.mjs "}),"."]})," O que ele mede, tudo por raycast\ncontra o mundo real:","\n",(0,r.jsxs)(o.ul,{children:["\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"MAP1"}),' \u2014 nenhum spawn e nenhum ch\xe3o and\xe1vel com o corpo dentro de geometria s\xf3lida.\nTeto = degrau de 0,30 m (acima disso n\xe3o \xe9 "passar por cima", \xe9 "estar dentro").']}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"MAP2"})," \u2014 cada time nasce todo no mesmo andar; respawn n\xe3o vis\xedvel de fora (medido com\no ",(0,r.jsx)(o.code,{children:"_losClear"})," ",(0,r.jsx)(o.strong,{children:"do jogo"}),", a mesma fun\xe7\xe3o que decide se o bot atira em voc\xea)."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"MAP3"})," \u2014 escada dentro da NBR 9077 / Blondel (espelho 16\u201318 cm, piso 25\u201332 cm,\n2h+p 63\u201365 cm, largura \u2265 1,20 m) ",(0,r.jsx)(o.strong,{children:"e"})," o grafo de navega\xe7\xe3o + o flood-fill sobem por\nela."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsx)(o.code,{children:"CTF1"})," \u2014 bandeiras n\xe3o colineares, \u2265 2 raios do spawn mais pr\xf3ximo, nenhuma enterrada."]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"node tools/eval/pickup-check.mjs"})]})," (alimenta a ",(0,r.jsx)(o.code,{children:"VM14"}),"): todo pickup precisa\nser alcan\xe7\xe1vel ",(0,r.jsx)(o.strong,{children:"a p\xe9"}),", por flood-fill de conectividade real em grade de 0,25 m\nsemeado nos spawns dos dois times. J\xe1 aconteceu de armas ca\xedrem dentro da piscina do\n",(0,r.jsx)(o.code,{children:"piscina_treta"})," com o quality gate marcando v\xe3o ",(0,r.jsx)(o.strong,{children:"0,0000 \u2014 VERDE"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Rode ",(0,r.jsx)(o.code,{children:"node tools/eval/botsim.mjs 60 "})]}),": os bots precisam navegar o seu mapa\nsem travar (",(0,r.jsx)(o.code,{children:"BOT3"})," stuck \u2264 4%), sem andar de lado (",(0,r.jsx)(o.code,{children:"BOT1"}),") e sem girar parados (",(0,r.jsx)(o.code,{children:"BOT2"}),').\nWaypoint desconexo \xe9 o defeito mais comum de mapa novo, e j\xe1 quebrou PRs antes\n(\xe9 o defeito que a dire\xe7\xe3o "conte\xfado como dado" existe para matar).']}),"\n"]}),"\n",(0,r.jsx)(o.h2,{id:"boas-primeiras-tarefas",children:"Boas primeiras tarefas"}),"\n",(0,r.jsx)(o.p,{children:"Ordenadas por (impacto \xf7 esfor\xe7o). Todas s\xe3o reais, verificadas nesta \xe1rvore, e nenhuma\nexige entender o jogo inteiro."}),"\n",(0,r.jsx)(o.h3,{id:"muito-boas-para-o-primeiro-pr",children:"Muito boas para o primeiro PR"}),"\n",(0,r.jsxs)(o.p,{children:["As tarefas de entrada moram em ",(0,r.jsx)(o.strong,{children:(0,r.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/tree/main/docs/issues",children:(0,r.jsx)(o.code,{children:"docs/issues/"})})}),",\numa por arquivo, cada uma com contexto, o que fazer, crit\xe9rio de aceite e quais arquivos\ntocar. O ",(0,r.jsx)(o.code,{children:"README.md"})," de l\xe1 indexa por tempo dispon\xedvel (30 min / 1 h / 2-3 h) e por \xe1rea\n(SEO, UI, backend, CI). ",(0,r.jsxs)(o.strong,{children:["Nenhuma delas exige tocar em ",(0,r.jsx)(o.code,{children:"public/js/*.js"})]}),", de prop\xf3sito:\n\xe9 o c\xf3digo onde os agentes de gameplay trabalham em paralelo e onde a tabela de conflito\ndo ",(0,r.jsx)(o.code,{children:"tools/eval/ARCH.md"})," manda."]}),"\n",(0,r.jsxs)(o.admonition,{title:"Elas ainda N\xc3O est\xe3o abertas no GitHub",type:"caution",children:[(0,r.jsxs)(o.p,{children:["Elas existem como arquivo, n\xe3o como issue. Existe um script pronto \u2014\n",(0,r.jsx)(o.code,{children:"docs/issues/abrir-issues.sh"}),", com ",(0,r.jsx)(o.a,{href:"https://cli.github.com/",children:(0,r.jsx)(o.code,{children:"gh"})})," autenticado:"]}),(0,r.jsx)(o.pre,{children:(0,r.jsx)(o.code,{className:"language-bash",children:"bash docs/issues/abrir-issues.sh --dry-run # imprime t\xedtulo + labels, n\xe3o abre nada\nbash docs/issues/abrir-issues.sh --labels # cria as 8 labels usadas\nbash docs/issues/abrir-issues.sh # abre as 15\n"})}),(0,r.jsxs)(o.p,{children:["Ele \xe9 idempotente (procura issue com o mesmo t\xedtulo antes de criar) e ",(0,r.jsx)(o.strong,{children:"nunca foi\nexecutado"}),": o reposit\xf3rio \xe9 do dono e abrir issue \xe9 a\xe7\xe3o irrevers\xedvel com o nome dele.\nOu seja, se voc\xea procurar as tarefas na aba Issues, n\xe3o vai achar \u2014 leia os ",(0,r.jsx)(o.code,{children:".md"}),"."]})]}),"\n",(0,r.jsxs)(o.admonition,{title:"Esta lista j\xe1 teve cinco itens, e quatro foram feitos",type:"note",children:[(0,r.jsxs)(o.p,{children:["Ela mandava corrigir o ",(0,r.jsx)(o.code,{children:"README.md"})," (feito), adicionar ",(0,r.jsx)(o.code,{children:"arch"}),"/",(0,r.jsx)(o.code,{children:"arch:check"})," ao\n",(0,r.jsx)(o.code,{children:"package.json"})," (existem hoje), regenerar o ",(0,r.jsx)(o.code,{children:"ARCH.md"})," e fazer o ",(0,r.jsx)(o.code,{children:"tp-mount-probe"})," pular\nquando faltasse ",(0,r.jsx)(o.code,{children:"public/models/anims/"})," \u2014 pasta que ",(0,r.jsx)(o.strong,{children:"hoje est\xe1 versionada"})," (438 arquivos\nem ",(0,r.jsx)(o.code,{children:"git ls-files public/models/anims"}),"). Doc que manda fazer o que j\xe1 foi feito queima a\nprimeira contribui\xe7\xe3o de algu\xe9m; por isso a lista virou ponteiro para ",(0,r.jsx)(o.code,{children:"docs/issues/"}),",\nque \xe9 mantida."]}),(0,r.jsxs)(o.p,{children:["O \xfanico item da lista antiga que ",(0,r.jsx)(o.strong,{children:"continua valendo"})," \u2014 e agora est\xe1 consertado:\na mensagem das invariantes PX1\u2013PX4 apontava para ",(0,r.jsx)(o.code,{children:"tools/eval/motion.mjs"}),', que\nnunca existiu no git (ponteiro fantasma). Hoje as skips declaram honestamente\n"sem arn\xeas dedicado (d\xedvida PX)": o que existe de browser no CI \xe9 o\n',(0,r.jsx)(o.code,{children:"portao-browser"})," (boot real do jogo + grafite + silhueta da sele\xe7\xe3o), e um\narn\xeas de viewmodel dedicado continua sendo trabalho aberto."]})]}),"\n",(0,r.jsx)(o.h3,{id:"trabalho-de-verdade-ainda-acess\xedvel",children:"Trabalho de verdade, ainda acess\xedvel"}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"VM12 e VM1 nas armas espec\xedficas."})," VM12 falha em 5 de 52 medidas (pior ",(0,r.jsx)(o.code,{children:"famas"}),"@3:2\ncom 0,660 contra o teto 0,62); VM1 em 2 de 26 (",(0,r.jsx)(o.code,{children:"famas"}),", ",(0,r.jsx)(o.code,{children:"uzi"}),"). S\xe3o corre\xe7\xf5es por arma,\ncom faixa medida e ",(0,r.jsx)(o.code,{children:"vm-solve.mjs"})," dispon\xedvel para provar viabilidade. ",(0,r.jsx)(o.em,{children:"Frente:\nARMAS/VIEWMODEL."})]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"BOT8 \u2014 bot com linha de vis\xe3o e sem atirar."})," \xc9 a d\xedvida mais barata da lista, e a\ncausa raiz j\xe1 est\xe1 achada: ",(0,r.jsx)(o.code,{children:"game.js:5361"})," avalia ",(0,r.jsx)(o.code,{children:"const hasTurn = \u2026 this._duelToken(b)"}),"\n",(0,r.jsx)(o.strong,{children:"todo frame"}),', antes de qualquer gate de "pode atirar" \u2014 e ',(0,r.jsx)(o.code,{children:"_duelToken"})," n\xe3o consulta,\nele ",(0,r.jsx)(o.strong,{children:"reserva"})," o token. Bot recarregando ou sem linha de tiro rouba um dos 2 tokens e\nsegura; os outros atravessam o campo de vis\xe3o sem disparar. A corre\xe7\xe3o \xe9 mover a chamada\npara dentro do ",(0,r.jsx)(o.code,{children:"if"}),". Medido na \xfaltima execu\xe7\xe3o registrada: ",(0,r.jsx)(o.strong,{children:"4 epis\xf3dios, sil\xeancio\nm\xe1ximo 4,23 s"})," \u2014 e note que ",(0,r.jsx)(o.strong,{children:"piorou"})," desde os 2,7 / 3,03 s do baseline, o que faz\ndela tamb\xe9m um bom A/B. ",(0,r.jsx)(o.em,{children:"Frente: BOTS/JOGABILIDADE. Detalhe: BUG-03."})]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"Personagens: propor\xe7\xe3o (CHR1) e mapas de superf\xedcie."})," Cuidado com doc velha aqui: a\n",(0,r.jsx)(o.strong,{children:"CHR5B ficou VERDE"})," em 04/08 (os 27 de 44 personagens sem mapa de superf\xedcie foram a\n",(0,r.jsx)(o.strong,{children:"0 de 44"}),"), ent\xe3o esse item espec\xedfico ",(0,r.jsx)(o.strong,{children:"j\xe1 foi feito"})," \u2014 n\xe3o o refa\xe7a. O que segue\nvermelho \xe9 CHR1/CHR3/CHR4, e a causa de fundo \xe9 rig, n\xe3o runtime (BUG-10). Leia o\nKNOWN-BUGS antes de pegar. ",(0,r.jsx)(o.em,{children:"Frente: PERSONAGENS."})]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsxs)(o.strong,{children:[(0,r.jsx)(o.code,{children:"setTimeout"})," n\xe3o limpos no ",(0,r.jsx)(o.code,{children:"dispose()"})]})," \u2014 vazamento entre partidas, apontado em\n",(0,r.jsx)(o.code,{children:"RELATORIO-ANALISE.md:134"}),". ",(0,r.jsx)(o.strong,{children:"Os n\xfameros de linha daquele relat\xf3rio est\xe3o velhos"})," (o\n",(0,r.jsx)(o.code,{children:"game.js"})," andou ~1.000 linhas desde ent\xe3o); ache os atuais com\n",(0,r.jsx)(o.code,{children:"grep -n setTimeout public/js/game.js"})," e confira quais sobrevivem ao ",(0,r.jsx)(o.code,{children:"dispose()"}),". Bom\nPR de higiene com efeito med\xedvel no heap. ",(0,r.jsxs)(o.em,{children:["Frente: zona vermelha ",(0,r.jsx)(o.code,{children:"constructor"}),"/",(0,r.jsx)(o.code,{children:"update"}),"\n\u2014 coordene antes."]})]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(o.h3,{id:"alto-valor-precisa-de-conversa-antes",children:"Alto valor, precisa de conversa antes"}),"\n",(0,r.jsxs)(o.ol,{start:"5",children:["\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsxs)(o.strong,{children:["Extrair ",(0,r.jsx)(o.code,{children:"_updateBot()"})," (772 linhas)."]})," Marcado como candidato a extra\xe7\xe3o pelo\npr\xf3prio \xedndice gerado. Precisa de acordo pr\xe9vio sobre a parti\xe7\xe3o, porque a regi\xe3o \xe9\ndisputada."]}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"Mapas como JSON (Fase 2)."})," Geometria, colliders, occluders, spawns, pickups e\nwaypoints em dado, com loader \xfanico e ",(0,r.jsx)(o.strong,{children:"waypoints validados por teste"}),'. \xc9 o que\ntransforma "PR de c\xf3digo arriscado" em "abre um JSON". Abra uma issue primeiro.']}),"\n"]}),"\n",(0,r.jsxs)(o.li,{children:["\n",(0,r.jsxs)(o.p,{children:[(0,r.jsx)(o.strong,{children:"Job de CI noturno com browser"})," para destravar PX1\u2013PX4. Quatro invariantes de pixel\nest\xe3o puladas desde sempre."]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(o.h2,{id:"processo",children:"Processo"}),"\n",(0,r.jsxs)(o.ol,{children:["\n",(0,r.jsxs)(o.li,{children:["Feature grande? ",(0,r.jsx)(o.strong,{children:"Abra uma issue antes"})," (veja ",(0,r.jsx)(o.code,{children:"IDEAS.md"}),")."]}),"\n",(0,r.jsxs)(o.li,{children:["Fork + branch ",(0,r.jsx)(o.strong,{children:(0,r.jsx)(o.code,{children:"v2/"})})," \u2014 ",(0,r.jsx)(o.code,{children:"v2/multiplayer"}),", ",(0,r.jsx)(o.code,{children:"v2/audio"}),", ",(0,r.jsx)(o.code,{children:"v2/ui-hud"}),". O prefixo\n\xe9 o ciclo de release (topo do ",(0,r.jsx)(o.code,{children:"CHANGELOG.md"}),"), e a conven\xe7\xe3o nasceu de um problema\nconcreto: em 04/08 a branch de trabalho ainda se chamava ",(0,r.jsx)(o.code,{children:"feat/evio-feel"})," \u2014 nome de uma\nfeature de julho \u2014 com ",(0,r.jsx)(o.strong,{children:"143 commits"})," de assuntos diferentes empilhados. Nome que n\xe3o\ndiz o que a branch \xe9 vira dep\xf3sito. (Fonte: ",(0,r.jsx)(o.code,{children:"CONTRIBUTING.md"}),".)"]}),"\n",(0,r.jsxs)(o.li,{children:["Rode ",(0,r.jsx)(o.code,{children:"npm run check"}),". Cole a sa\xedda no PR."]}),"\n",(0,r.jsxs)(o.li,{children:["PR pequeno, uma frente, descri\xe7\xe3o com n\xfameros e ",(0,r.jsx)(o.code,{children:"arquivo:linha"}),"."]}),"\n",(0,r.jsxs)(o.li,{children:[(0,r.jsxs)(o.strong,{children:["Ao contribuir voc\xea licencia sob a licen\xe7a que o ",(0,r.jsx)(o.code,{children:"LICENSE"})," disser no momento do seu\nPR."]})," Qual \xe9 ela hoje e quais arquivos mudam junto numa troca: a se\xe7\xe3o de licen\xe7a do\n",(0,r.jsx)(o.code,{children:"CONTRIBUTING.md"}),". Se isso\nfor decisivo pra voc\xea, leia l\xe1 antes de escrever a primeira linha."]}),"\n"]}),"\n",(0,r.jsxs)(o.p,{children:["Reportando bug: o que aconteceu, o que esperava, passos pra reproduzir, navegador/SO e\nprint do console (F12). E se o bug for de comportamento, ele vai virar invariante \u2014 \xe9\nassim que ele nunca volta (",(0,r.jsx)(o.code,{children:"tools/eval/invariants.mjs:20-21"}),")."]})]})}function m(e={}){const{wrapper:o}={...(0,n.R)(),...e.components};return o?(0,r.jsx)(o,{...e,children:(0,r.jsx)(t,{...e})}):t(e)}},8453(e,o,a){a.d(o,{R:()=>d,x:()=>i});var s=a(6540);const r={},n=s.createContext(r);function d(e){const o=s.useContext(n);return s.useMemo(function(){return"function"==typeof e?e(o):{...o,...e}},[o,e])}function i(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),s.createElement(n.Provider,{value:o},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/assets/js/44959d42.590f9b1e.js b/public/docs/assets/js/44959d42.ed546c78.js similarity index 98% rename from public/docs/assets/js/44959d42.590f9b1e.js rename to public/docs/assets/js/44959d42.ed546c78.js index d635bb4c1..9918be234 100644 --- a/public/docs/assets/js/44959d42.590f9b1e.js +++ b/public/docs/assets/js/44959d42.ed546c78.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[796],{2616(e,o,s){s.r(o),s.d(o,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>l});const r=JSON.parse('{"id":"arquitetura","title":"Arquitetura: N agentes no mesmo arquivo","description":"A arquitetura de verdade, gerada por tools/gen-arch.mjs \u2014 e o mecanismo de faixas de linha disjuntas que permite agentes em paralelo sem colis\xe3o.","source":"@site/docs/arquitetura.md","sourceDirName":".","slug":"/arquitetura","permalink":"/docs/arquitetura","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/arquitetura.md","tags":[],"version":"current","sidebarPosition":5,"frontMatter":{"id":"arquitetura","title":"Arquitetura: N agentes no mesmo arquivo","sidebar_label":"Arquitetura","sidebar_position":5,"description":"A arquitetura de verdade, gerada por tools/gen-arch.mjs \u2014 e o mecanismo de faixas de linha disjuntas que permite agentes em paralelo sem colis\xe3o."},"sidebar":"dev","previous":{"title":"BotBrain","permalink":"/docs/botbrain"},"next":{"title":"Como colaborar","permalink":"/docs/colaborar"}}');var n=s(4848),d=s(8453);const a={id:"arquitetura",title:"Arquitetura: N agentes no mesmo arquivo",sidebar_label:"Arquitetura",sidebar_position:5,description:"A arquitetura de verdade, gerada por tools/gen-arch.mjs \u2014 e o mecanismo de faixas de linha disjuntas que permite agentes em paralelo sem colis\xe3o."},i="Arquitetura: N agentes no mesmo arquivo",c={},l=[{value:"Por que este documento \xe9 gerado por script",id:"por-que-este-documento-\xe9-gerado-por-script",level:2},{value:"Os arquivos indexados",id:"os-arquivos-indexados",level:2},{value:"Os maiores m\xe9todos de game.js \u2014 onde o conflito mora",id:"os-maiores-m\xe9todos-de-gamejs--onde-o-conflito-mora",level:3},{value:"Faixas de linha disjuntas",id:"faixas-de-linha-disjuntas",level:2},{value:"Como funciona",id:"como-funciona",level:3},{value:"A tabela de conflito",id:"a-tabela-de-conflito",level:3},{value:"As zonas vermelhas",id:"as-zonas-vermelhas",level:3},{value:"As regras operacionais",id:"as-regras-operacionais",level:3},{value:"As tr\xeas zonas do reposit\xf3rio",id:"as-tr\xeas-zonas-do-reposit\xf3rio",level:2},{value:"Consequ\xeancia pr\xe1tica",id:"consequ\xeancia-pr\xe1tica",level:3},{value:"Sistema de dados de conte\xfado",id:"sistema-de-dados-de-conte\xfado",level:2},{value:"O que \xe9 gerado, e o que n\xe3o \xe9",id:"o-que-\xe9-gerado-e-o-que-n\xe3o-\xe9",level:2},{value:"O que o gerador N\xc3O resolve: ponteiros arquivo:linha na prosa",id:"o-que-o-gerador-n\xe3o-resolve-ponteiros-arquivolinha-na-prosa",level:3}];function t(e){const o={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(o.header,{children:(0,n.jsx)(o.h1,{id:"arquitetura-n-agentes-no-mesmo-arquivo",children:"Arquitetura: N agentes no mesmo arquivo"})}),"\n",(0,n.jsx)(o.h2,{id:"por-que-este-documento-\xe9-gerado-por-script",children:"Por que este documento \xe9 gerado por script"}),"\n",(0,n.jsxs)(o.p,{children:["O ",(0,n.jsx)(o.code,{children:"tools/eval/ARCH.md"})," n\xe3o \xe9 escrito \xe0 m\xe3o. Ele \xe9 gerado por ",(0,n.jsx)(o.code,{children:"node tools/gen-arch.mjs"}),",\ne a raz\xe3o est\xe1 no cabe\xe7alho do script (",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:5-8"}),"):"]}),"\n",(0,n.jsxs)(o.blockquote,{children:["\n",(0,n.jsxs)(o.p,{children:["O ",(0,n.jsx)(o.code,{children:"ARCH.md"}),' escrito \xe0 m\xe3o dizia "game.js (3234 linhas)" quando o arquivo tinha 5361.\nTodos os ponteiros ',(0,n.jsx)(o.code,{children:"arquivo:linha"})," da tabela de conflito estavam deslocados \u2014 e essa\ntabela \xe9 justamente o que impede dois agentes (ou dois contribuidores) de editarem a\nmesma regi\xe3o. Um \xedndice por n\xfamero de linha escrito \xe0 m\xe3o desatualiza no primeiro\ncommit; a \xfanica corre\xe7\xe3o \xe9 gerar."]}),"\n"]}),"\n",(0,n.jsxs)(o.p,{children:["E a separa\xe7\xe3o que faz isso funcionar (",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:11-13"}),"):"]}),"\n",(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{children:"frente -> S\xcdMBOLO = conhecimento humano, est\xe1vel, vive nas FRENTES do script\ns\xedmbolo -> LINHA = vol\xe1til, \xe9 o que este script resolve toda vez\n"})}),"\n",(0,n.jsxs)(o.p,{children:["O ",(0,n.jsx)(o.code,{children:"ARCH.md"})," antigo cravava ",(0,n.jsx)(o.strong,{children:"frente \u2192 linha"}),", misturando os dois prazos de validade. \xc9\numa ideia pequena com consequ\xeancia grande: a parti\xe7\xe3o de trabalho \xe9 declarada em termos\nque n\xe3o mudam (nomes de m\xe9todo), e a resolu\xe7\xe3o para coordenadas vol\xe1teis (n\xfameros de\nlinha) \xe9 recalculada a cada execu\xe7\xe3o."]}),"\n",(0,n.jsxs)(o.admonition,{type:"note",children:[(0,n.jsxs)(o.mdxAdmonitionTitle,{children:["O ",(0,n.jsx)(o.code,{children:"arch:check"})," est\xe1 VERMELHO agora \u2014 e isso \xe9 a melhor demonstra\xe7\xe3o da p\xe1gina"]}),(0,n.jsxs)(o.p,{children:[(0,n.jsx)(o.code,{children:"npm run arch"})," e ",(0,n.jsx)(o.code,{children:"npm run arch:check"})," existem hoje no ",(0,n.jsx)(o.code,{children:"package.json"})," da raiz, e o cheque\nn\xe3o est\xe1 passando:"]}),(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{children:"$ npm run arch:check\n\u2717 ARCH1 ARCH.md est\xe1 DESATUALIZADO em rela\xe7\xe3o ao c\xf3digo.\n game.js tem 6428 linhas; o \xedndice do ARCH.md n\xe3o bate.\n Rode: npm run arch\n"})}),(0,n.jsxs)(o.p,{children:["A mensagem induz ao erro de prop\xf3sito: ela ",(0,n.jsx)(o.strong,{children:"fala de linhas porque \xe9 o resumo que sabe\nimprimir"}),", mas o que o ",(0,n.jsx)(o.code,{children:"--check"})," compara \xe9 o bloco gerado inteiro, byte a byte \u2014 e esse\nbloco carrega tamb\xe9m o n\xfamero de vers\xe3o do jogo. \xcdndice de s\xedmbolo certo e vers\xe3o velha d\xe1\na mesma vermelha. Um comando resolve."]}),(0,n.jsxs)(o.p,{children:["Cuidado que continua valendo: no CI o passo est\xe1 com ",(0,n.jsx)(o.code,{children:"continue-on-error: true"}),", ent\xe3o o\ncheque roda mas ",(0,n.jsx)(o.strong,{children:"n\xe3o bloqueia"})," \u2014 foi exatamente por isso que ele conseguiu ficar\nvermelho sem que ningu\xe9m percebesse. Tirar essa linha \xe9 o que o transforma em quality gate de\nverdade."]})]}),"\n",(0,n.jsx)(o.h2,{id:"os-arquivos-indexados",children:"Os arquivos indexados"}),"\n",(0,n.jsxs)(o.p,{children:["Tamanho dos arquivos que o ",(0,n.jsx)(o.code,{children:"gen-arch.mjs"})," indexa \u2014 bloco gerado, regenerado por\n",(0,n.jsx)(o.code,{children:"npm run docs"})," e conferido por ",(0,n.jsx)(o.code,{children:"npm run docs:check"}),":"]}),"\n","\n",(0,n.jsxs)(o.table,{children:[(0,n.jsx)(o.thead,{children:(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.th,{children:"Arquivo"}),(0,n.jsx)(o.th,{style:{textAlign:"right"},children:"Linhas"})]})}),(0,n.jsxs)(o.tbody,{children:[(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/game.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"6.838"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/main.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"2.646"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/characters.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"1.068"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/glbchars.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"837"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/vmattach.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"628"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/weapons.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"344"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/springs.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"260"})]})]})]}),"\n",(0,n.jsxs)(o.p,{children:["Total de ",(0,n.jsx)(o.code,{children:"public/js/"}),": ",(0,n.jsx)(o.strong,{children:"31.744 linhas em 44 arquivos"}),". O \xedndice s\xedmbolo\u2192linha, com a tabela de conflito, \xe9 outro bloco gerado: ",(0,n.jsx)(o.code,{children:"tools/eval/ARCH.md"})," (",(0,n.jsx)(o.code,{children:"npm run arch"}),")."]}),"\n",(0,n.jsxs)(o.blockquote,{children:["\n",(0,n.jsxs)(o.p,{children:["Bloco gerado por ",(0,n.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(o.code,{children:"git ls-files public/js/*.js | xargs wc -l"})]}),"\n"]}),"\n","\n",(0,n.jsxs)(o.h3,{id:"os-maiores-m\xe9todos-de-gamejs--onde-o-conflito-mora",children:["Os maiores m\xe9todos de ",(0,n.jsx)(o.code,{children:"game.js"})," \u2014 onde o conflito mora"]}),"\n",(0,n.jsxs)(o.p,{children:["Esta tabela ",(0,n.jsx)(o.strong,{children:"n\xe3o \xe9 reproduzida aqui"}),", e a raz\xe3o \xe9 a pr\xf3pria tese da p\xe1gina: ela \xe9\n",(0,n.jsx)(o.code,{children:"linha \u2192 m\xe9todo"}),", o lado vol\xe1til da separa\xe7\xe3o, e duplic\xe1-la numa p\xe1gina de prosa cria uma\nsegunda c\xf3pia que envelhece sozinha. Ela vive gerada, num lugar s\xf3:"]}),"\n",(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{className:"language-bash",children:"npm run arch # regenera tools/eval/ARCH.md\nnode tools/gen-arch.mjs --json # o \xedndice cru, para outra ferramenta\n"})}),"\n",(0,n.jsxs)(o.p,{children:["O que ",(0,n.jsx)(o.strong,{children:"n\xe3o"})," envelhece, e por isso fica escrito aqui: ",(0,n.jsx)(o.code,{children:"_updateBot()"})," \xe9 de longe o maior\nm\xe9todo do arquivo e est\xe1 marcado pelo pr\xf3prio \xedndice como ",(0,n.jsx)(o.strong,{children:"candidato a extra\xe7\xe3o"}),";\n",(0,n.jsx)(o.code,{children:"constructor()"}),", ",(0,n.jsx)(o.code,{children:"update()"})," e ",(0,n.jsx)(o.code,{children:"_dom()"})," s\xe3o ",(0,n.jsx)(o.strong,{children:"zona vermelha, append-only"}),", porque qualquer\nfrente pode precisar deles. M\xe9todo grande = PR irrevis\xe1vel e merge conflitante \u2014 extrair\n",(0,n.jsx)(o.code,{children:"_updateBot"})," \xe9 trabalho de valor alto e risco m\xe9dio, e exige coordenar antes, porque a\nregi\xe3o \xe9 disputada."]}),"\n",(0,n.jsx)(o.h2,{id:"faixas-de-linha-disjuntas",children:"Faixas de linha disjuntas"}),"\n",(0,n.jsxs)(o.p,{children:["Este \xe9 o mecanismo que permite v\xe1rios agentes (ou contribuidores) editarem o ",(0,n.jsx)(o.strong,{children:"mesmo\narquivo"})," \u2014 o maior do reposit\xf3rio, com milhares de linhas \u2014 ao mesmo tempo, sem conflito\nde merge."]}),"\n",(0,n.jsx)(o.h3,{id:"como-funciona",children:"Como funciona"}),"\n",(0,n.jsxs)(o.ol,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Cada frente declara S\xcdMBOLOS, nunca linhas."})," Em ",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:32-73"}),", a\nconstante ",(0,n.jsx)(o.code,{children:"FRENTES"})," lista, por frente, tr\xeas coisas: ",(0,n.jsx)(o.code,{children:"arquivos"})," exclusivos, ",(0,n.jsx)(o.code,{children:"simbolos"}),"\n(m\xe9todos) e ",(0,n.jsx)(o.code,{children:"consts"})," (constantes de topo). Exemplo, a frente ARMAS/VIEWMODEL possui\n",(0,n.jsx)(o.code,{children:"_buildViewModels"}),", ",(0,n.jsx)(o.code,{children:"_vmFrame"}),", ",(0,n.jsx)(o.code,{children:"_tryShoot"}),", ",(0,n.jsx)(o.code,{children:"_shotRecoil"}),"\u2026 e as constantes ",(0,n.jsx)(o.code,{children:"WEAPONS"}),",\n",(0,n.jsx)(o.code,{children:"VM_FOV_DEFAULT"}),", ",(0,n.jsx)(o.code,{children:"VM_OFF"}),", ",(0,n.jsx)(o.code,{children:"REC_DEG"}),"."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"O script indexa o arquivo e resolve s\xedmbolo \u2192 faixa."})," ",(0,n.jsx)(o.code,{children:"indexar()"}),"\n(",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:80-111"}),") varre o arquivo linha a linha com tr\xeas padr\xf5es: m\xe9todo\nde classe (exatamente 2 espa\xe7os de indenta\xe7\xe3o), ",(0,n.jsx)(o.strong,{children:"m\xe9todo-arrow atribu\xeddo em runtime"}),"\n(",(0,n.jsx)(o.code,{children:"this._vmFrame = (force) => {"}),") e declara\xe7\xe3o de topo. O fim de cada s\xedmbolo \xe9 o\nin\xedcio do pr\xf3ximo."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Faixas cont\xedguas s\xe3o fundidas"})," (gap \u2264 12 linhas) para a tabela ficar leg\xedvel\n(",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:172-178"}),")."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Sobreposi\xe7\xe3o entre frentes \xe9 detectada"}),", porque uma tabela de conflito que se\ncontradiz \xe9 pior que nenhuma (",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:190-200"}),")."]}),"\n"]}),"\n",(0,n.jsxs)(o.p,{children:["O detalhe do passo 2 vale destacar: a v1 do script s\xf3 via m\xe9todos de classe, e por isso\n",(0,n.jsx)(o.code,{children:"_vmFrame"})," \u2014 cerca de 100 linhas que nascem ",(0,n.jsx)(o.strong,{children:"dentro"})," de outro m\xe9todo, como arrow que\nfecha sobre vari\xe1veis locais \u2014 ficava ",(0,n.jsx)(o.strong,{children:"invis\xedvel no \xedndice"}),"\n(",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:95-97"}),"). Um \xedndice que n\xe3o v\xea o m\xe9todo mais disputado do arquivo \xe9\npior que nenhum \xedndice, porque d\xe1 falsa confian\xe7a."]}),"\n",(0,n.jsx)(o.h3,{id:"a-tabela-de-conflito",children:"A tabela de conflito"}),"\n",(0,n.jsxs)(o.p,{children:["Do ",(0,n.jsx)(o.code,{children:"tools/eval/ARCH.md"})," (bloco gerado \u2014 as faixas abaixo s\xe3o as da gera\xe7\xe3o anterior; rode\n",(0,n.jsx)(o.code,{children:"node tools/gen-arch.mjs"})," para as de hoje):"]}),"\n",(0,n.jsxs)(o.table,{children:[(0,n.jsx)(o.thead,{children:(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.th,{children:"Frente"}),(0,n.jsx)(o.th,{children:"Arquivos exclusivos"})]})}),(0,n.jsxs)(o.tbody,{children:[(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"ARMAS / VIEWMODEL"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"vmattach.js"})," ",(0,n.jsx)(o.code,{children:"springs.js"})," ",(0,n.jsx)(o.code,{children:"weapons.js"})," ",(0,n.jsx)(o.code,{children:"fparms.js"})," ",(0,n.jsx)(o.code,{children:"handik.js"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"BOTS / JOGABILIDADE"})}),(0,n.jsxs)(o.td,{children:["\u2014 (s\xf3 faixas em ",(0,n.jsx)(o.code,{children:"game.js"}),")"]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"MAPAS / MUNDO"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"maps.js"})," ",(0,n.jsx)(o.code,{children:"mapprops.js"})," ",(0,n.jsx)(o.code,{children:"map_brasilia.js"})," ",(0,n.jsx)(o.code,{children:"map_havan.js"})," ",(0,n.jsx)(o.code,{children:"map_piscina.js"})," ",(0,n.jsx)(o.code,{children:"map_piscinao_ramos.js"})," ",(0,n.jsx)(o.code,{children:"map_ferrovelho.js"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"GR\xc1FICOS / FX"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"bloom.js"})," ",(0,n.jsx)(o.code,{children:"textures.js"})," ",(0,n.jsx)(o.code,{children:"vao.js"})," ",(0,n.jsx)(o.code,{children:"stylize.js"})," ",(0,n.jsx)(o.code,{children:"gpuparticles.js"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"UI / HUD / MENU"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"main.js"})," ",(0,n.jsx)(o.code,{children:"public/style.css"})," ",(0,n.jsx)(o.code,{children:"src/pages/index.astro"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"\xc1UDIO"})}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"audio.js"})})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"PERSONAGENS"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"characters.js"})," ",(0,n.jsx)(o.code,{children:"glbchars.js"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"SITE / BACKEND"})}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"src/"})})]})]})]}),"\n",(0,n.jsxs)(o.admonition,{title:"Dois arquivos de mapa N\xc3O t\xeam dono declarado",type:"caution",children:[(0,n.jsxs)(o.p,{children:[(0,n.jsx)(o.code,{children:"map_quebrada.js"})," (1.319 linhas, o mapa mais novo) e ",(0,n.jsx)(o.code,{children:"map_decals.js"})," ",(0,n.jsx)(o.strong,{children:"n\xe3o aparecem em\nfrente nenhuma"})," de ",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs"})," \u2014 a lista acima \xe9 c\xf3pia fiel do ",(0,n.jsx)(o.code,{children:"FRENTES"}),", e eles\nn\xe3o est\xe3o l\xe1. Quem editar os dois n\xe3o colide com ningu\xe9m ",(0,n.jsx)(o.em,{children:"segundo a tabela"}),", que \xe9\njustamente a garantia que a tabela deveria dar e n\xe3o d\xe1. Acrescent\xe1-los \xe9 uma linha em\n",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs"})," seguida de ",(0,n.jsx)(o.code,{children:"npm run arch"}),"."]}),(0,n.jsxs)(o.p,{children:["(O ",(0,n.jsx)(o.code,{children:"map.js"})," j\xe1 foi listado aqui e ",(0,n.jsx)(o.strong,{children:"n\xe3o existe mais"}),': era a "Pra\xe7a (cl\xe1ssico)", apagada\njunto com o mapa ',(0,n.jsx)(o.code,{children:"praca_old"}),".)"]})]}),"\n",(0,n.jsx)(o.h3,{id:"as-zonas-vermelhas",children:"As zonas vermelhas"}),"\n",(0,n.jsxs)(o.p,{children:["Tr\xeas m\xe9todos s\xe3o ",(0,n.jsx)(o.strong,{children:"append-only"}),", porque qualquer frente pode precisar deles\n(",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:75-77"}),"):"]}),"\n",(0,n.jsxs)(o.ul,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.code,{children:"update()"})," \u2014 o loop"]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.code,{children:"_dom()"})," \u2014 o wiring de HUD"]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.code,{children:"constructor()"})," \u2014 um dos maiores m\xe9todos do arquivo (o tamanho de hoje est\xe1 no ",(0,n.jsx)(o.code,{children:"ARCH.md"}),")"]}),"\n"]}),"\n",(0,n.jsx)(o.p,{children:"Editar o miolo destes \xe9 o jeito mais r\xe1pido de dois contribuidores se atropelarem.\nAcrescente no fim; n\xe3o reorganize."}),"\n",(0,n.jsx)(o.h3,{id:"as-regras-operacionais",children:"As regras operacionais"}),"\n",(0,n.jsxs)(o.ul,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Declare sua frente antes de editar."})," Se for um PR humano, diga na descri\xe7\xe3o."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsxs)(o.strong,{children:["Em ",(0,n.jsx)(o.code,{children:"game.js"}),", use edi\xe7\xe3o por trecho \u2014 nunca sobrescreva o arquivo inteiro."]})," Uma\nferramenta que reescreve o arquivo apaga o trabalho de quem est\xe1 na outra faixa."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Duas frentes com faixas disjuntas rodam em paralelo."})," O ",(0,n.jsx)(o.code,{children:"ARCH.md"})," gerado registra\nque isso foi medido: ",(0,n.jsx)(o.em,{children:'"3 agentes editaram faixas disjuntas simultaneamente com zero\nconflito de conte\xfado"'})," (",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:163"}),")."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Mexeu num s\xedmbolo? Mova o nome na declara\xe7\xe3o da frente, n\xe3o o n\xfamero."})," O script\navisa quando um s\xedmbolo declarado some do c\xf3digo."]}),"\n"]}),"\n",(0,n.jsx)(o.admonition,{title:"Por que isso importa pra voc\xea, humano",type:"tip",children:(0,n.jsxs)(o.p,{children:["A mesma parti\xe7\xe3o que evita colis\xe3o entre agentes \xe9 o que torna um PR seu revis\xe1vel. Um\nPR que toca ",(0,n.jsx)(o.code,{children:"_updateBot"})," + ",(0,n.jsx)(o.code,{children:"style.css"})," + ",(0,n.jsx)(o.code,{children:"map_havan.js"})," \xe9 tr\xeas PRs escondidos num s\xf3, e\nvai colidir com tr\xeas frentes diferentes. Um PR por frente entra r\xe1pido."]})}),"\n",(0,n.jsx)(o.h2,{id:"as-tr\xeas-zonas-do-reposit\xf3rio",children:"As tr\xeas zonas do reposit\xf3rio"}),"\n",(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{children:"public/ jogo vanilla ES modules, zero build, Three.js vendorizado\nsrc/ site Astro + adapter Vercel, API routes SSR\ntools/ arn\xeas scripts .mjs/.py \u2014 a r\xe9gua, o quality gate e as sondas\n"})}),"\n",(0,n.jsxs)(o.p,{children:["Vers\xf5es, contagens e o que cada ferramenta faz est\xe3o em\n",(0,n.jsx)(o.a,{href:"/docs/stack",children:"Stack e ferramentas"})," \u2014 ",(0,n.jsx)(o.strong,{children:"gerado"}),", n\xe3o escrito \xe0 m\xe3o."]}),"\n",(0,n.jsx)(o.p,{children:"O acoplamento entre elas \xe9 deliberadamente fino e vale entender:"}),"\n",(0,n.jsxs)(o.ul,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"O site carrega o jogo por import map"}),", em ",(0,n.jsx)(o.code,{children:"src/pages/index.astro:97-123"}),". \xc9 o \xfanico\nponto onde o Astro sabe da exist\xeancia dos m\xf3dulos do jogo."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"O arn\xeas carrega o jogo direto do disco"}),", sem browser: ",(0,n.jsx)(o.code,{children:"tools/eval/harness.mjs"})," stuba\nDOM/canvas/",(0,n.jsx)(o.code,{children:"fetch"})," e importa ",(0,n.jsx)(o.code,{children:"public/js/game.js"})," como m\xf3dulo. Por isso o quality gate mede o\nc\xf3digo de produ\xe7\xe3o, e n\xe3o uma reimplementa\xe7\xe3o."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:(0,n.jsx)(o.code,{children:"tools/eval/serve.mjs:15"})})," faz a ponte pro caso de teste: serve ",(0,n.jsx)(o.code,{children:"public/"})," e mapeia\n",(0,n.jsx)(o.code,{children:"/"})," para o fonte do ",(0,n.jsx)(o.code,{children:"index.astro"}),", sem Astro no caminho."]}),"\n"]}),"\n",(0,n.jsx)(o.h3,{id:"consequ\xeancia-pr\xe1tica",children:"Consequ\xeancia pr\xe1tica"}),"\n",(0,n.jsxs)(o.p,{children:["O jogo ",(0,n.jsx)(o.strong,{children:"n\xe3o pode"})," ganhar depend\xeancia de runtime nem passo de build. Isso n\xe3o \xe9\nconservadorismo: \xe9 o que faz ",(0,n.jsx)(o.code,{children:"harness.mjs"})," conseguir subir a classe ",(0,n.jsx)(o.code,{children:"Game"})," em node puro\nem segundos, que \xe9 o que faz o quality gate existir. Um bundler no meio quebraria a r\xe9gua junto\ncom a portabilidade."]}),"\n",(0,n.jsx)(o.h2,{id:"sistema-de-dados-de-conte\xfado",children:"Sistema de dados de conte\xfado"}),"\n",(0,n.jsxs)(o.p,{children:["Hoje mapas, armas e personagens s\xe3o ",(0,n.jsx)(o.strong,{children:"c\xf3digo"}),": cada ",(0,n.jsx)(o.code,{children:"map_*.js"}),' \xe9 geometria declarada \xe0\nm\xe3o, e os maiores deles rivalizam em tamanho com os m\xf3dulos de sistema. A dire\xe7\xe3o\n"conte\xfado como dado" do\n',(0,n.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,n.jsx)(o.code,{children:"docs/ROADMAP.md"})}),"\nquer migrar isso para JSON com loader \xfanico, para que uma contribui\xe7\xe3o de conte\xfado seja\n",(0,n.jsx)(o.em,{children:'"abre um JSON e cria conte\xfado"'})," em vez de ",(0,n.jsx)(o.em,{children:'"um PR de c\xf3digo hand-coded arriscado"'}),"."]}),"\n",(0,n.jsxs)(o.p,{children:["Se voc\xea quer o trabalho de maior alavancagem no projeto inteiro, \xe9 esse. Ver\n",(0,n.jsx)(o.a,{href:"/docs/estado",children:"Estado atual"}),"."]}),"\n",(0,n.jsx)(o.h2,{id:"o-que-\xe9-gerado-e-o-que-n\xe3o-\xe9",children:"O que \xe9 gerado, e o que n\xe3o \xe9"}),"\n",(0,n.jsx)(o.p,{children:"Duas coisas neste reposit\xf3rio s\xe3o geradas por script, e pela mesma raz\xe3o:"}),"\n",(0,n.jsxs)(o.table,{children:[(0,n.jsx)(o.thead,{children:(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.th,{children:"Gerado"}),(0,n.jsx)(o.th,{children:"Script"}),(0,n.jsx)(o.th,{children:"Quality gate"})]})}),(0,n.jsxs)(o.tbody,{children:[(0,n.jsxs)(o.tr,{children:[(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"tools/eval/ARCH.md"})," \u2014 \xedndice s\xedmbolo\u2192linha e tabela de conflito"]}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs"})}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"npm run arch:check"})})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsxs)(o.td,{children:["Os blocos num\xe9ricos de ",(0,n.jsx)(o.code,{children:"README.md"})," e desta documenta\xe7\xe3o"]}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"tools/gen-docs.mjs"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"npm run docs:check"})," (no ",(0,n.jsx)(o.code,{children:"check:fast"}),")"]})]})]})]}),"\n",(0,n.jsx)(o.p,{children:"A regra que separa o que entra e o que fica:"}),"\n",(0,n.jsxs)(o.ul,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Deriv\xe1vel do c\xf3digo?"})," Vira bloco gerado, entre marcadores, com ",(0,n.jsx)(o.code,{children:"--check"})," no quality gate.\nContagem de linhas, de personagens, de armas, de mapas, de scripts, de invariantes,\nvers\xe3o, lista de scripts do ",(0,n.jsx)(o.code,{children:"package.json"}),", vers\xe3o de depend\xeancia."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"N\xe3o deriv\xe1vel?"})," Ent\xe3o \xe9 decis\xe3o ou explica\xe7\xe3o \u2014 e ",(0,n.jsx)(o.strong,{children:"n\xe3o deve conter n\xfamero que\nenvelhece"}),". Escreva sem o n\xfamero, ou cite o comando que o produz. O placar do quality gate,\npor exemplo, depende de qual insumo existe na m\xe1quina: ele mora colado de uma execu\xe7\xe3o\nreal no ",(0,n.jsx)(o.code,{children:"KNOWN-BUGS.md"}),", n\xe3o repetido em cinco p\xe1ginas."]}),"\n"]}),"\n",(0,n.jsxs)(o.p,{children:["E o motivo de o ",(0,n.jsx)(o.code,{children:"--check"})," estar no quality gate, n\xe3o s\xf3 dispon\xedvel: ",(0,n.jsx)(o.strong,{children:"o que n\xe3o vira r\xe9gua \xe9\notimizado para fora."})," Um gerador que ningu\xe9m \xe9 obrigado a rodar desatualiza em uma semana,\ne a\xed a documenta\xe7\xe3o volta a mentir com a apar\xeancia de rigor \u2014 que \xe9 pior do que mentir sem\nela."]}),"\n",(0,n.jsxs)(o.admonition,{title:"Onde voc\xea p\xf5e o quality gate novo na corrente importa",type:"danger",children:[(0,n.jsxs)(o.p,{children:["O ",(0,n.jsx)(o.code,{children:"check:fast"})," \xe9 uma corrente de ",(0,n.jsx)(o.code,{children:"&&"}),": o primeiro erro corta o resto. O ",(0,n.jsx)(o.code,{children:"arch:check"})," est\xe1\nvermelho h\xe1 dias, ent\xe3o ",(0,n.jsx)(o.strong,{children:"todo quality gate colocado depois dele nasce morto"})," \u2014 roda zero vezes\ne ningu\xe9m percebe, porque a sa\xedda para antes. Foi exatamente o que aconteceu na primeira\nvers\xe3o do ",(0,n.jsx)(o.code,{children:"docs:check"}),", e \xe9 o mesmo modo de falha do BUG-02 (o quality gate medindo o viewmodel\nde ontem porque o ",(0,n.jsx)(o.code,{children:"&&"})," cortava antes de o JSON ser regenerado)."]}),(0,n.jsxs)(o.p,{children:["Por isso o ",(0,n.jsx)(o.code,{children:"docs:check"})," vem ",(0,n.jsx)(o.strong,{children:"antes"})," do ",(0,n.jsx)(o.code,{children:"arch:check"})," no ",(0,n.jsx)(o.code,{children:"package.json"}),", com o motivo\nescrito no ",(0,n.jsx)(o.code,{children:"SCRIPTS.md"})," (chave ",(0,n.jsx)(o.code,{children:"check:fast"}),"). Quando o ",(0,n.jsx)(o.code,{children:"ARCH.md"})," for regenerado e o ",(0,n.jsx)(o.code,{children:"arch:check"})," voltar\na verde, a ordem deixa de importar; at\xe9 l\xe1, importa."]})]}),"\n",(0,n.jsxs)(o.p,{children:["Colar um bloco novo \xe9 escrever o marcador e rodar ",(0,n.jsx)(o.code,{children:"npm run docs"}),":"]}),"\n",(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{children:"{/* BEGIN:GERADO:NOME_DO_BLOCO \u2014 n\xe3o edite \xe0 m\xe3o, rode `npm run docs` */}\n{/* END:GERADO:NOME_DO_BLOCO */}\n"})}),"\n",(0,n.jsxs)(o.p,{children:["(",(0,n.jsx)(o.code,{children:"NOME_DO_BLOCO"})," \xe9 uma das chaves do objeto ",(0,n.jsx)(o.code,{children:"BLOCOS"})," no topo do ",(0,n.jsx)(o.code,{children:"gen-docs.mjs"}),". Bloco\ndeclarado que ningu\xe9m consome vira aviso alto na sa\xedda \u2014 bloco \xf3rf\xe3o \xe9 c\xf3digo morto que\nfinge ser documenta\xe7\xe3o.)"]}),"\n",(0,n.jsxs)(o.p,{children:["Em Markdown puro (",(0,n.jsx)(o.code,{children:"README.md"}),") o marcador \xe9 coment\xe1rio HTML (",(0,n.jsx)(o.code,{children:"\x3c!-- BEGIN:GERADO:\u2026 --\x3e"}),").\nNas p\xe1ginas desta doc \xe9 coment\xe1rio ",(0,n.jsx)(o.strong,{children:"MDX"})," (",(0,n.jsx)(o.code,{children:"{/* \u2026 */}"}),"): o Docusaurus 3 compila ",(0,n.jsx)(o.code,{children:".md"}),"\ncomo MDX, e coment\xe1rio HTML ali \xe9 erro de parse que derruba o build. O gerador aceita as\nduas sintaxes e preserva a que encontrar."]}),"\n",(0,n.jsxs)(o.h3,{id:"o-que-o-gerador-n\xe3o-resolve-ponteiros-arquivolinha-na-prosa",children:["O que o gerador N\xc3O resolve: ponteiros ",(0,n.jsx)(o.code,{children:"arquivo:linha"})," na prosa"]}),"\n",(0,n.jsxs)(o.p,{children:["Um ",(0,n.jsx)(o.code,{children:"game.js:5361"})," escrito no meio de um par\xe1grafo \xe9 a vers\xe3o barata do mesmo defeito \u2014 ele\naponta pro lugar errado no primeiro commit que mexer no arquivo. N\xe3o d\xe1 pra gerar (o\nponteiro faz parte da frase), mas d\xe1 pra ",(0,n.jsx)(o.strong,{children:"detectar o caso grosseiro"}),": ponteiro que aponta\npara al\xe9m do fim do arquivo."]}),"\n","\n",(0,n.jsxs)(o.p,{children:["Nenhum ponteiro ",(0,n.jsx)(o.code,{children:"arquivo:linha"})," das docs aponta para fora do arquivo que ele cita. \u2713"]}),"\n",(0,n.jsxs)(o.blockquote,{children:["\n",(0,n.jsxs)(o.p,{children:["Isto confere s\xf3 o ",(0,n.jsx)(o.strong,{children:"limite"})," do arquivo: um ponteiro que ainda cabe mas mudou de assunto passa aqui. \xc9 a raz\xe3o de a doutrina da casa ser declarar o S\xcdMBOLO e deixar a linha para o gerador \u2014 ver ",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs"}),"."]}),"\n"]}),"\n",(0,n.jsxs)(o.blockquote,{children:["\n",(0,n.jsxs)(o.p,{children:["Bloco gerado por ",(0,n.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(o.code,{children:"varredura de "}),"arquivo",":linha",(0,n.jsx)(o.code,{children:" em README/STATUS/HANDOFF/KNOWN-BUGS/docs/docs/SKILL"})]}),"\n"]}),"\n","\n",(0,n.jsxs)(o.p,{children:["Por isso a doutrina \xe9 declarar o ",(0,n.jsx)(o.strong,{children:"s\xedmbolo"})," e deixar a linha para o gerador. Quando o\n",(0,n.jsx)(o.code,{children:"arquivo:linha"})," for mesmo necess\xe1rio, cite junto o nome do que est\xe1 l\xe1 \u2014 assim quem ler\ndaqui a um m\xeas acha por ",(0,n.jsx)(o.code,{children:"grep"})," mesmo com o ponteiro deslocado."]})]})}function h(e={}){const{wrapper:o}={...(0,d.R)(),...e.components};return o?(0,n.jsx)(o,{...e,children:(0,n.jsx)(t,{...e})}):t(e)}},8453(e,o,s){s.d(o,{R:()=>a,x:()=>i});var r=s(6540);const n={},d=r.createContext(n);function a(e){const o=r.useContext(d);return r.useMemo(function(){return"function"==typeof e?e(o):{...o,...e}},[o,e])}function i(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),r.createElement(d.Provider,{value:o},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[796],{2616(e,o,s){s.r(o),s.d(o,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>l});const r=JSON.parse('{"id":"arquitetura","title":"Arquitetura: N agentes no mesmo arquivo","description":"A arquitetura de verdade, gerada por tools/gen-arch.mjs \u2014 e o mecanismo de faixas de linha disjuntas que permite agentes em paralelo sem colis\xe3o.","source":"@site/docs/arquitetura.md","sourceDirName":".","slug":"/arquitetura","permalink":"/docs/arquitetura","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/arquitetura.md","tags":[],"version":"current","sidebarPosition":5,"frontMatter":{"id":"arquitetura","title":"Arquitetura: N agentes no mesmo arquivo","sidebar_label":"Arquitetura","sidebar_position":5,"description":"A arquitetura de verdade, gerada por tools/gen-arch.mjs \u2014 e o mecanismo de faixas de linha disjuntas que permite agentes em paralelo sem colis\xe3o."},"sidebar":"dev","previous":{"title":"BotBrain","permalink":"/docs/botbrain"},"next":{"title":"Como colaborar","permalink":"/docs/colaborar"}}');var n=s(4848),d=s(8453);const a={id:"arquitetura",title:"Arquitetura: N agentes no mesmo arquivo",sidebar_label:"Arquitetura",sidebar_position:5,description:"A arquitetura de verdade, gerada por tools/gen-arch.mjs \u2014 e o mecanismo de faixas de linha disjuntas que permite agentes em paralelo sem colis\xe3o."},i="Arquitetura: N agentes no mesmo arquivo",c={},l=[{value:"Por que este documento \xe9 gerado por script",id:"por-que-este-documento-\xe9-gerado-por-script",level:2},{value:"Os arquivos indexados",id:"os-arquivos-indexados",level:2},{value:"Os maiores m\xe9todos de game.js \u2014 onde o conflito mora",id:"os-maiores-m\xe9todos-de-gamejs--onde-o-conflito-mora",level:3},{value:"Faixas de linha disjuntas",id:"faixas-de-linha-disjuntas",level:2},{value:"Como funciona",id:"como-funciona",level:3},{value:"A tabela de conflito",id:"a-tabela-de-conflito",level:3},{value:"As zonas vermelhas",id:"as-zonas-vermelhas",level:3},{value:"As regras operacionais",id:"as-regras-operacionais",level:3},{value:"As tr\xeas zonas do reposit\xf3rio",id:"as-tr\xeas-zonas-do-reposit\xf3rio",level:2},{value:"Consequ\xeancia pr\xe1tica",id:"consequ\xeancia-pr\xe1tica",level:3},{value:"Sistema de dados de conte\xfado",id:"sistema-de-dados-de-conte\xfado",level:2},{value:"O que \xe9 gerado, e o que n\xe3o \xe9",id:"o-que-\xe9-gerado-e-o-que-n\xe3o-\xe9",level:2},{value:"O que o gerador N\xc3O resolve: ponteiros arquivo:linha na prosa",id:"o-que-o-gerador-n\xe3o-resolve-ponteiros-arquivolinha-na-prosa",level:3}];function t(e){const o={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(o.header,{children:(0,n.jsx)(o.h1,{id:"arquitetura-n-agentes-no-mesmo-arquivo",children:"Arquitetura: N agentes no mesmo arquivo"})}),"\n",(0,n.jsx)(o.h2,{id:"por-que-este-documento-\xe9-gerado-por-script",children:"Por que este documento \xe9 gerado por script"}),"\n",(0,n.jsxs)(o.p,{children:["O ",(0,n.jsx)(o.code,{children:"tools/eval/ARCH.md"})," n\xe3o \xe9 escrito \xe0 m\xe3o. Ele \xe9 gerado por ",(0,n.jsx)(o.code,{children:"node tools/gen-arch.mjs"}),",\ne a raz\xe3o est\xe1 no cabe\xe7alho do script (",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:5-8"}),"):"]}),"\n",(0,n.jsxs)(o.blockquote,{children:["\n",(0,n.jsxs)(o.p,{children:["O ",(0,n.jsx)(o.code,{children:"ARCH.md"}),' escrito \xe0 m\xe3o dizia "game.js (3234 linhas)" quando o arquivo tinha 5361.\nTodos os ponteiros ',(0,n.jsx)(o.code,{children:"arquivo:linha"})," da tabela de conflito estavam deslocados \u2014 e essa\ntabela \xe9 justamente o que impede dois agentes (ou dois contribuidores) de editarem a\nmesma regi\xe3o. Um \xedndice por n\xfamero de linha escrito \xe0 m\xe3o desatualiza no primeiro\ncommit; a \xfanica corre\xe7\xe3o \xe9 gerar."]}),"\n"]}),"\n",(0,n.jsxs)(o.p,{children:["E a separa\xe7\xe3o que faz isso funcionar (",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:11-13"}),"):"]}),"\n",(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{children:"frente -> S\xcdMBOLO = conhecimento humano, est\xe1vel, vive nas FRENTES do script\ns\xedmbolo -> LINHA = vol\xe1til, \xe9 o que este script resolve toda vez\n"})}),"\n",(0,n.jsxs)(o.p,{children:["O ",(0,n.jsx)(o.code,{children:"ARCH.md"})," antigo cravava ",(0,n.jsx)(o.strong,{children:"frente \u2192 linha"}),", misturando os dois prazos de validade. \xc9\numa ideia pequena com consequ\xeancia grande: a parti\xe7\xe3o de trabalho \xe9 declarada em termos\nque n\xe3o mudam (nomes de m\xe9todo), e a resolu\xe7\xe3o para coordenadas vol\xe1teis (n\xfameros de\nlinha) \xe9 recalculada a cada execu\xe7\xe3o."]}),"\n",(0,n.jsxs)(o.admonition,{type:"note",children:[(0,n.jsxs)(o.mdxAdmonitionTitle,{children:["O ",(0,n.jsx)(o.code,{children:"arch:check"})," est\xe1 VERMELHO agora \u2014 e isso \xe9 a melhor demonstra\xe7\xe3o da p\xe1gina"]}),(0,n.jsxs)(o.p,{children:[(0,n.jsx)(o.code,{children:"npm run arch"})," e ",(0,n.jsx)(o.code,{children:"npm run arch:check"})," existem hoje no ",(0,n.jsx)(o.code,{children:"package.json"})," da raiz, e o cheque\nn\xe3o est\xe1 passando:"]}),(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{children:"$ npm run arch:check\n\u2717 ARCH1 ARCH.md est\xe1 DESATUALIZADO em rela\xe7\xe3o ao c\xf3digo.\n game.js tem 6428 linhas; o \xedndice do ARCH.md n\xe3o bate.\n Rode: npm run arch\n"})}),(0,n.jsxs)(o.p,{children:["A mensagem induz ao erro de prop\xf3sito: ela ",(0,n.jsx)(o.strong,{children:"fala de linhas porque \xe9 o resumo que sabe\nimprimir"}),", mas o que o ",(0,n.jsx)(o.code,{children:"--check"})," compara \xe9 o bloco gerado inteiro, byte a byte \u2014 e esse\nbloco carrega tamb\xe9m o n\xfamero de vers\xe3o do jogo. \xcdndice de s\xedmbolo certo e vers\xe3o velha d\xe1\na mesma vermelha. Um comando resolve."]}),(0,n.jsxs)(o.p,{children:["Cuidado que continua valendo: no CI o passo est\xe1 com ",(0,n.jsx)(o.code,{children:"continue-on-error: true"}),", ent\xe3o o\ncheque roda mas ",(0,n.jsx)(o.strong,{children:"n\xe3o bloqueia"})," \u2014 foi exatamente por isso que ele conseguiu ficar\nvermelho sem que ningu\xe9m percebesse. Tirar essa linha \xe9 o que o transforma em quality gate de\nverdade."]})]}),"\n",(0,n.jsx)(o.h2,{id:"os-arquivos-indexados",children:"Os arquivos indexados"}),"\n",(0,n.jsxs)(o.p,{children:["Tamanho dos arquivos que o ",(0,n.jsx)(o.code,{children:"gen-arch.mjs"})," indexa \u2014 bloco gerado, regenerado por\n",(0,n.jsx)(o.code,{children:"npm run docs"})," e conferido por ",(0,n.jsx)(o.code,{children:"npm run docs:check"}),":"]}),"\n","\n",(0,n.jsxs)(o.table,{children:[(0,n.jsx)(o.thead,{children:(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.th,{children:"Arquivo"}),(0,n.jsx)(o.th,{style:{textAlign:"right"},children:"Linhas"})]})}),(0,n.jsxs)(o.tbody,{children:[(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/game.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"6.910"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/main.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"2.698"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/characters.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"1.068"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/glbchars.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"844"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/vmattach.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"628"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/weapons.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"346"})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"public/js/springs.js"})}),(0,n.jsx)(o.td,{style:{textAlign:"right"},children:"260"})]})]})]}),"\n",(0,n.jsxs)(o.p,{children:["Total de ",(0,n.jsx)(o.code,{children:"public/js/"}),": ",(0,n.jsx)(o.strong,{children:"32.001 linhas em 44 arquivos"}),". O \xedndice s\xedmbolo\u2192linha, com a tabela de conflito, \xe9 outro bloco gerado: ",(0,n.jsx)(o.code,{children:"tools/eval/ARCH.md"})," (",(0,n.jsx)(o.code,{children:"npm run arch"}),")."]}),"\n",(0,n.jsxs)(o.blockquote,{children:["\n",(0,n.jsxs)(o.p,{children:["Bloco gerado por ",(0,n.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(o.code,{children:"git ls-files public/js/*.js | xargs wc -l"})]}),"\n"]}),"\n","\n",(0,n.jsxs)(o.h3,{id:"os-maiores-m\xe9todos-de-gamejs--onde-o-conflito-mora",children:["Os maiores m\xe9todos de ",(0,n.jsx)(o.code,{children:"game.js"})," \u2014 onde o conflito mora"]}),"\n",(0,n.jsxs)(o.p,{children:["Esta tabela ",(0,n.jsx)(o.strong,{children:"n\xe3o \xe9 reproduzida aqui"}),", e a raz\xe3o \xe9 a pr\xf3pria tese da p\xe1gina: ela \xe9\n",(0,n.jsx)(o.code,{children:"linha \u2192 m\xe9todo"}),", o lado vol\xe1til da separa\xe7\xe3o, e duplic\xe1-la numa p\xe1gina de prosa cria uma\nsegunda c\xf3pia que envelhece sozinha. Ela vive gerada, num lugar s\xf3:"]}),"\n",(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{className:"language-bash",children:"npm run arch # regenera tools/eval/ARCH.md\nnode tools/gen-arch.mjs --json # o \xedndice cru, para outra ferramenta\n"})}),"\n",(0,n.jsxs)(o.p,{children:["O que ",(0,n.jsx)(o.strong,{children:"n\xe3o"})," envelhece, e por isso fica escrito aqui: ",(0,n.jsx)(o.code,{children:"_updateBot()"})," \xe9 de longe o maior\nm\xe9todo do arquivo e est\xe1 marcado pelo pr\xf3prio \xedndice como ",(0,n.jsx)(o.strong,{children:"candidato a extra\xe7\xe3o"}),";\n",(0,n.jsx)(o.code,{children:"constructor()"}),", ",(0,n.jsx)(o.code,{children:"update()"})," e ",(0,n.jsx)(o.code,{children:"_dom()"})," s\xe3o ",(0,n.jsx)(o.strong,{children:"zona vermelha, append-only"}),", porque qualquer\nfrente pode precisar deles. M\xe9todo grande = PR irrevis\xe1vel e merge conflitante \u2014 extrair\n",(0,n.jsx)(o.code,{children:"_updateBot"})," \xe9 trabalho de valor alto e risco m\xe9dio, e exige coordenar antes, porque a\nregi\xe3o \xe9 disputada."]}),"\n",(0,n.jsx)(o.h2,{id:"faixas-de-linha-disjuntas",children:"Faixas de linha disjuntas"}),"\n",(0,n.jsxs)(o.p,{children:["Este \xe9 o mecanismo que permite v\xe1rios agentes (ou contribuidores) editarem o ",(0,n.jsx)(o.strong,{children:"mesmo\narquivo"})," \u2014 o maior do reposit\xf3rio, com milhares de linhas \u2014 ao mesmo tempo, sem conflito\nde merge."]}),"\n",(0,n.jsx)(o.h3,{id:"como-funciona",children:"Como funciona"}),"\n",(0,n.jsxs)(o.ol,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Cada frente declara S\xcdMBOLOS, nunca linhas."})," Em ",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:32-73"}),", a\nconstante ",(0,n.jsx)(o.code,{children:"FRENTES"})," lista, por frente, tr\xeas coisas: ",(0,n.jsx)(o.code,{children:"arquivos"})," exclusivos, ",(0,n.jsx)(o.code,{children:"simbolos"}),"\n(m\xe9todos) e ",(0,n.jsx)(o.code,{children:"consts"})," (constantes de topo). Exemplo, a frente ARMAS/VIEWMODEL possui\n",(0,n.jsx)(o.code,{children:"_buildViewModels"}),", ",(0,n.jsx)(o.code,{children:"_vmFrame"}),", ",(0,n.jsx)(o.code,{children:"_tryShoot"}),", ",(0,n.jsx)(o.code,{children:"_shotRecoil"}),"\u2026 e as constantes ",(0,n.jsx)(o.code,{children:"WEAPONS"}),",\n",(0,n.jsx)(o.code,{children:"VM_FOV_DEFAULT"}),", ",(0,n.jsx)(o.code,{children:"VM_OFF"}),", ",(0,n.jsx)(o.code,{children:"REC_DEG"}),"."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"O script indexa o arquivo e resolve s\xedmbolo \u2192 faixa."})," ",(0,n.jsx)(o.code,{children:"indexar()"}),"\n(",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:80-111"}),") varre o arquivo linha a linha com tr\xeas padr\xf5es: m\xe9todo\nde classe (exatamente 2 espa\xe7os de indenta\xe7\xe3o), ",(0,n.jsx)(o.strong,{children:"m\xe9todo-arrow atribu\xeddo em runtime"}),"\n(",(0,n.jsx)(o.code,{children:"this._vmFrame = (force) => {"}),") e declara\xe7\xe3o de topo. O fim de cada s\xedmbolo \xe9 o\nin\xedcio do pr\xf3ximo."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Faixas cont\xedguas s\xe3o fundidas"})," (gap \u2264 12 linhas) para a tabela ficar leg\xedvel\n(",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:172-178"}),")."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Sobreposi\xe7\xe3o entre frentes \xe9 detectada"}),", porque uma tabela de conflito que se\ncontradiz \xe9 pior que nenhuma (",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:190-200"}),")."]}),"\n"]}),"\n",(0,n.jsxs)(o.p,{children:["O detalhe do passo 2 vale destacar: a v1 do script s\xf3 via m\xe9todos de classe, e por isso\n",(0,n.jsx)(o.code,{children:"_vmFrame"})," \u2014 cerca de 100 linhas que nascem ",(0,n.jsx)(o.strong,{children:"dentro"})," de outro m\xe9todo, como arrow que\nfecha sobre vari\xe1veis locais \u2014 ficava ",(0,n.jsx)(o.strong,{children:"invis\xedvel no \xedndice"}),"\n(",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:95-97"}),"). Um \xedndice que n\xe3o v\xea o m\xe9todo mais disputado do arquivo \xe9\npior que nenhum \xedndice, porque d\xe1 falsa confian\xe7a."]}),"\n",(0,n.jsx)(o.h3,{id:"a-tabela-de-conflito",children:"A tabela de conflito"}),"\n",(0,n.jsxs)(o.p,{children:["Do ",(0,n.jsx)(o.code,{children:"tools/eval/ARCH.md"})," (bloco gerado \u2014 as faixas abaixo s\xe3o as da gera\xe7\xe3o anterior; rode\n",(0,n.jsx)(o.code,{children:"node tools/gen-arch.mjs"})," para as de hoje):"]}),"\n",(0,n.jsxs)(o.table,{children:[(0,n.jsx)(o.thead,{children:(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.th,{children:"Frente"}),(0,n.jsx)(o.th,{children:"Arquivos exclusivos"})]})}),(0,n.jsxs)(o.tbody,{children:[(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"ARMAS / VIEWMODEL"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"vmattach.js"})," ",(0,n.jsx)(o.code,{children:"springs.js"})," ",(0,n.jsx)(o.code,{children:"weapons.js"})," ",(0,n.jsx)(o.code,{children:"fparms.js"})," ",(0,n.jsx)(o.code,{children:"handik.js"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"BOTS / JOGABILIDADE"})}),(0,n.jsxs)(o.td,{children:["\u2014 (s\xf3 faixas em ",(0,n.jsx)(o.code,{children:"game.js"}),")"]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"MAPAS / MUNDO"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"maps.js"})," ",(0,n.jsx)(o.code,{children:"mapprops.js"})," ",(0,n.jsx)(o.code,{children:"map_brasilia.js"})," ",(0,n.jsx)(o.code,{children:"map_havan.js"})," ",(0,n.jsx)(o.code,{children:"map_piscina.js"})," ",(0,n.jsx)(o.code,{children:"map_piscinao_ramos.js"})," ",(0,n.jsx)(o.code,{children:"map_ferrovelho.js"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"GR\xc1FICOS / FX"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"bloom.js"})," ",(0,n.jsx)(o.code,{children:"textures.js"})," ",(0,n.jsx)(o.code,{children:"vao.js"})," ",(0,n.jsx)(o.code,{children:"stylize.js"})," ",(0,n.jsx)(o.code,{children:"gpuparticles.js"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"UI / HUD / MENU"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"main.js"})," ",(0,n.jsx)(o.code,{children:"public/style.css"})," ",(0,n.jsx)(o.code,{children:"src/pages/index.astro"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"\xc1UDIO"})}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"audio.js"})})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"PERSONAGENS"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"characters.js"})," ",(0,n.jsx)(o.code,{children:"glbchars.js"})]})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.td,{children:(0,n.jsx)(o.strong,{children:"SITE / BACKEND"})}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"src/"})})]})]})]}),"\n",(0,n.jsxs)(o.admonition,{title:"Dois arquivos de mapa N\xc3O t\xeam dono declarado",type:"caution",children:[(0,n.jsxs)(o.p,{children:[(0,n.jsx)(o.code,{children:"map_quebrada.js"})," (1.319 linhas, o mapa mais novo) e ",(0,n.jsx)(o.code,{children:"map_decals.js"})," ",(0,n.jsx)(o.strong,{children:"n\xe3o aparecem em\nfrente nenhuma"})," de ",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs"})," \u2014 a lista acima \xe9 c\xf3pia fiel do ",(0,n.jsx)(o.code,{children:"FRENTES"}),", e eles\nn\xe3o est\xe3o l\xe1. Quem editar os dois n\xe3o colide com ningu\xe9m ",(0,n.jsx)(o.em,{children:"segundo a tabela"}),", que \xe9\njustamente a garantia que a tabela deveria dar e n\xe3o d\xe1. Acrescent\xe1-los \xe9 uma linha em\n",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs"})," seguida de ",(0,n.jsx)(o.code,{children:"npm run arch"}),"."]}),(0,n.jsxs)(o.p,{children:["(O ",(0,n.jsx)(o.code,{children:"map.js"})," j\xe1 foi listado aqui e ",(0,n.jsx)(o.strong,{children:"n\xe3o existe mais"}),': era a "Pra\xe7a (cl\xe1ssico)", apagada\njunto com o mapa ',(0,n.jsx)(o.code,{children:"praca_old"}),".)"]})]}),"\n",(0,n.jsx)(o.h3,{id:"as-zonas-vermelhas",children:"As zonas vermelhas"}),"\n",(0,n.jsxs)(o.p,{children:["Tr\xeas m\xe9todos s\xe3o ",(0,n.jsx)(o.strong,{children:"append-only"}),", porque qualquer frente pode precisar deles\n(",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:75-77"}),"):"]}),"\n",(0,n.jsxs)(o.ul,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.code,{children:"update()"})," \u2014 o loop"]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.code,{children:"_dom()"})," \u2014 o wiring de HUD"]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.code,{children:"constructor()"})," \u2014 um dos maiores m\xe9todos do arquivo (o tamanho de hoje est\xe1 no ",(0,n.jsx)(o.code,{children:"ARCH.md"}),")"]}),"\n"]}),"\n",(0,n.jsx)(o.p,{children:"Editar o miolo destes \xe9 o jeito mais r\xe1pido de dois contribuidores se atropelarem.\nAcrescente no fim; n\xe3o reorganize."}),"\n",(0,n.jsx)(o.h3,{id:"as-regras-operacionais",children:"As regras operacionais"}),"\n",(0,n.jsxs)(o.ul,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Declare sua frente antes de editar."})," Se for um PR humano, diga na descri\xe7\xe3o."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsxs)(o.strong,{children:["Em ",(0,n.jsx)(o.code,{children:"game.js"}),", use edi\xe7\xe3o por trecho \u2014 nunca sobrescreva o arquivo inteiro."]})," Uma\nferramenta que reescreve o arquivo apaga o trabalho de quem est\xe1 na outra faixa."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Duas frentes com faixas disjuntas rodam em paralelo."})," O ",(0,n.jsx)(o.code,{children:"ARCH.md"})," gerado registra\nque isso foi medido: ",(0,n.jsx)(o.em,{children:'"3 agentes editaram faixas disjuntas simultaneamente com zero\nconflito de conte\xfado"'})," (",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs:163"}),")."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Mexeu num s\xedmbolo? Mova o nome na declara\xe7\xe3o da frente, n\xe3o o n\xfamero."})," O script\navisa quando um s\xedmbolo declarado some do c\xf3digo."]}),"\n"]}),"\n",(0,n.jsx)(o.admonition,{title:"Por que isso importa pra voc\xea, humano",type:"tip",children:(0,n.jsxs)(o.p,{children:["A mesma parti\xe7\xe3o que evita colis\xe3o entre agentes \xe9 o que torna um PR seu revis\xe1vel. Um\nPR que toca ",(0,n.jsx)(o.code,{children:"_updateBot"})," + ",(0,n.jsx)(o.code,{children:"style.css"})," + ",(0,n.jsx)(o.code,{children:"map_havan.js"})," \xe9 tr\xeas PRs escondidos num s\xf3, e\nvai colidir com tr\xeas frentes diferentes. Um PR por frente entra r\xe1pido."]})}),"\n",(0,n.jsx)(o.h2,{id:"as-tr\xeas-zonas-do-reposit\xf3rio",children:"As tr\xeas zonas do reposit\xf3rio"}),"\n",(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{children:"public/ jogo vanilla ES modules, zero build, Three.js vendorizado\nsrc/ site Astro + adapter Vercel, API routes SSR\ntools/ arn\xeas scripts .mjs/.py \u2014 a r\xe9gua, o quality gate e as sondas\n"})}),"\n",(0,n.jsxs)(o.p,{children:["Vers\xf5es, contagens e o que cada ferramenta faz est\xe3o em\n",(0,n.jsx)(o.a,{href:"/docs/stack",children:"Stack e ferramentas"})," \u2014 ",(0,n.jsx)(o.strong,{children:"gerado"}),", n\xe3o escrito \xe0 m\xe3o."]}),"\n",(0,n.jsx)(o.p,{children:"O acoplamento entre elas \xe9 deliberadamente fino e vale entender:"}),"\n",(0,n.jsxs)(o.ul,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"O site carrega o jogo por import map"}),", em ",(0,n.jsx)(o.code,{children:"src/pages/index.astro:97-123"}),". \xc9 o \xfanico\nponto onde o Astro sabe da exist\xeancia dos m\xf3dulos do jogo."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"O arn\xeas carrega o jogo direto do disco"}),", sem browser: ",(0,n.jsx)(o.code,{children:"tools/eval/harness.mjs"})," stuba\nDOM/canvas/",(0,n.jsx)(o.code,{children:"fetch"})," e importa ",(0,n.jsx)(o.code,{children:"public/js/game.js"})," como m\xf3dulo. Por isso o quality gate mede o\nc\xf3digo de produ\xe7\xe3o, e n\xe3o uma reimplementa\xe7\xe3o."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:(0,n.jsx)(o.code,{children:"tools/eval/serve.mjs:15"})})," faz a ponte pro caso de teste: serve ",(0,n.jsx)(o.code,{children:"public/"})," e mapeia\n",(0,n.jsx)(o.code,{children:"/"})," para o fonte do ",(0,n.jsx)(o.code,{children:"index.astro"}),", sem Astro no caminho."]}),"\n"]}),"\n",(0,n.jsx)(o.h3,{id:"consequ\xeancia-pr\xe1tica",children:"Consequ\xeancia pr\xe1tica"}),"\n",(0,n.jsxs)(o.p,{children:["O jogo ",(0,n.jsx)(o.strong,{children:"n\xe3o pode"})," ganhar depend\xeancia de runtime nem passo de build. Isso n\xe3o \xe9\nconservadorismo: \xe9 o que faz ",(0,n.jsx)(o.code,{children:"harness.mjs"})," conseguir subir a classe ",(0,n.jsx)(o.code,{children:"Game"})," em node puro\nem segundos, que \xe9 o que faz o quality gate existir. Um bundler no meio quebraria a r\xe9gua junto\ncom a portabilidade."]}),"\n",(0,n.jsx)(o.h2,{id:"sistema-de-dados-de-conte\xfado",children:"Sistema de dados de conte\xfado"}),"\n",(0,n.jsxs)(o.p,{children:["Hoje mapas, armas e personagens s\xe3o ",(0,n.jsx)(o.strong,{children:"c\xf3digo"}),": cada ",(0,n.jsx)(o.code,{children:"map_*.js"}),' \xe9 geometria declarada \xe0\nm\xe3o, e os maiores deles rivalizam em tamanho com os m\xf3dulos de sistema. A dire\xe7\xe3o\n"conte\xfado como dado" do\n',(0,n.jsx)(o.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,n.jsx)(o.code,{children:"docs/ROADMAP.md"})}),"\nquer migrar isso para JSON com loader \xfanico, para que uma contribui\xe7\xe3o de conte\xfado seja\n",(0,n.jsx)(o.em,{children:'"abre um JSON e cria conte\xfado"'})," em vez de ",(0,n.jsx)(o.em,{children:'"um PR de c\xf3digo hand-coded arriscado"'}),"."]}),"\n",(0,n.jsxs)(o.p,{children:["Se voc\xea quer o trabalho de maior alavancagem no projeto inteiro, \xe9 esse. Ver\n",(0,n.jsx)(o.a,{href:"/docs/estado",children:"Estado atual"}),"."]}),"\n",(0,n.jsx)(o.h2,{id:"o-que-\xe9-gerado-e-o-que-n\xe3o-\xe9",children:"O que \xe9 gerado, e o que n\xe3o \xe9"}),"\n",(0,n.jsx)(o.p,{children:"Duas coisas neste reposit\xf3rio s\xe3o geradas por script, e pela mesma raz\xe3o:"}),"\n",(0,n.jsxs)(o.table,{children:[(0,n.jsx)(o.thead,{children:(0,n.jsxs)(o.tr,{children:[(0,n.jsx)(o.th,{children:"Gerado"}),(0,n.jsx)(o.th,{children:"Script"}),(0,n.jsx)(o.th,{children:"Quality gate"})]})}),(0,n.jsxs)(o.tbody,{children:[(0,n.jsxs)(o.tr,{children:[(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"tools/eval/ARCH.md"})," \u2014 \xedndice s\xedmbolo\u2192linha e tabela de conflito"]}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs"})}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"npm run arch:check"})})]}),(0,n.jsxs)(o.tr,{children:[(0,n.jsxs)(o.td,{children:["Os blocos num\xe9ricos de ",(0,n.jsx)(o.code,{children:"README.md"})," e desta documenta\xe7\xe3o"]}),(0,n.jsx)(o.td,{children:(0,n.jsx)(o.code,{children:"tools/gen-docs.mjs"})}),(0,n.jsxs)(o.td,{children:[(0,n.jsx)(o.code,{children:"npm run docs:check"})," (no ",(0,n.jsx)(o.code,{children:"check:fast"}),")"]})]})]})]}),"\n",(0,n.jsx)(o.p,{children:"A regra que separa o que entra e o que fica:"}),"\n",(0,n.jsxs)(o.ul,{children:["\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"Deriv\xe1vel do c\xf3digo?"})," Vira bloco gerado, entre marcadores, com ",(0,n.jsx)(o.code,{children:"--check"})," no quality gate.\nContagem de linhas, de personagens, de armas, de mapas, de scripts, de invariantes,\nvers\xe3o, lista de scripts do ",(0,n.jsx)(o.code,{children:"package.json"}),", vers\xe3o de depend\xeancia."]}),"\n",(0,n.jsxs)(o.li,{children:[(0,n.jsx)(o.strong,{children:"N\xe3o deriv\xe1vel?"})," Ent\xe3o \xe9 decis\xe3o ou explica\xe7\xe3o \u2014 e ",(0,n.jsx)(o.strong,{children:"n\xe3o deve conter n\xfamero que\nenvelhece"}),". Escreva sem o n\xfamero, ou cite o comando que o produz. O placar do quality gate,\npor exemplo, depende de qual insumo existe na m\xe1quina: ele mora colado de uma execu\xe7\xe3o\nreal no ",(0,n.jsx)(o.code,{children:"KNOWN-BUGS.md"}),", n\xe3o repetido em cinco p\xe1ginas."]}),"\n"]}),"\n",(0,n.jsxs)(o.p,{children:["E o motivo de o ",(0,n.jsx)(o.code,{children:"--check"})," estar no quality gate, n\xe3o s\xf3 dispon\xedvel: ",(0,n.jsx)(o.strong,{children:"o que n\xe3o vira r\xe9gua \xe9\notimizado para fora."})," Um gerador que ningu\xe9m \xe9 obrigado a rodar desatualiza em uma semana,\ne a\xed a documenta\xe7\xe3o volta a mentir com a apar\xeancia de rigor \u2014 que \xe9 pior do que mentir sem\nela."]}),"\n",(0,n.jsxs)(o.admonition,{title:"Onde voc\xea p\xf5e o quality gate novo na corrente importa",type:"danger",children:[(0,n.jsxs)(o.p,{children:["O ",(0,n.jsx)(o.code,{children:"check:fast"})," \xe9 uma corrente de ",(0,n.jsx)(o.code,{children:"&&"}),": o primeiro erro corta o resto. O ",(0,n.jsx)(o.code,{children:"arch:check"})," est\xe1\nvermelho h\xe1 dias, ent\xe3o ",(0,n.jsx)(o.strong,{children:"todo quality gate colocado depois dele nasce morto"})," \u2014 roda zero vezes\ne ningu\xe9m percebe, porque a sa\xedda para antes. Foi exatamente o que aconteceu na primeira\nvers\xe3o do ",(0,n.jsx)(o.code,{children:"docs:check"}),", e \xe9 o mesmo modo de falha do BUG-02 (o quality gate medindo o viewmodel\nde ontem porque o ",(0,n.jsx)(o.code,{children:"&&"})," cortava antes de o JSON ser regenerado)."]}),(0,n.jsxs)(o.p,{children:["Por isso o ",(0,n.jsx)(o.code,{children:"docs:check"})," vem ",(0,n.jsx)(o.strong,{children:"antes"})," do ",(0,n.jsx)(o.code,{children:"arch:check"})," no ",(0,n.jsx)(o.code,{children:"package.json"}),", com o motivo\nescrito no ",(0,n.jsx)(o.code,{children:"SCRIPTS.md"})," (chave ",(0,n.jsx)(o.code,{children:"check:fast"}),"). Quando o ",(0,n.jsx)(o.code,{children:"ARCH.md"})," for regenerado e o ",(0,n.jsx)(o.code,{children:"arch:check"})," voltar\na verde, a ordem deixa de importar; at\xe9 l\xe1, importa."]})]}),"\n",(0,n.jsxs)(o.p,{children:["Colar um bloco novo \xe9 escrever o marcador e rodar ",(0,n.jsx)(o.code,{children:"npm run docs"}),":"]}),"\n",(0,n.jsx)(o.pre,{children:(0,n.jsx)(o.code,{children:"{/* BEGIN:GERADO:NOME_DO_BLOCO \u2014 n\xe3o edite \xe0 m\xe3o, rode `npm run docs` */}\n{/* END:GERADO:NOME_DO_BLOCO */}\n"})}),"\n",(0,n.jsxs)(o.p,{children:["(",(0,n.jsx)(o.code,{children:"NOME_DO_BLOCO"})," \xe9 uma das chaves do objeto ",(0,n.jsx)(o.code,{children:"BLOCOS"})," no topo do ",(0,n.jsx)(o.code,{children:"gen-docs.mjs"}),". Bloco\ndeclarado que ningu\xe9m consome vira aviso alto na sa\xedda \u2014 bloco \xf3rf\xe3o \xe9 c\xf3digo morto que\nfinge ser documenta\xe7\xe3o.)"]}),"\n",(0,n.jsxs)(o.p,{children:["Em Markdown puro (",(0,n.jsx)(o.code,{children:"README.md"}),") o marcador \xe9 coment\xe1rio HTML (",(0,n.jsx)(o.code,{children:"\x3c!-- BEGIN:GERADO:\u2026 --\x3e"}),").\nNas p\xe1ginas desta doc \xe9 coment\xe1rio ",(0,n.jsx)(o.strong,{children:"MDX"})," (",(0,n.jsx)(o.code,{children:"{/* \u2026 */}"}),"): o Docusaurus 3 compila ",(0,n.jsx)(o.code,{children:".md"}),"\ncomo MDX, e coment\xe1rio HTML ali \xe9 erro de parse que derruba o build. O gerador aceita as\nduas sintaxes e preserva a que encontrar."]}),"\n",(0,n.jsxs)(o.h3,{id:"o-que-o-gerador-n\xe3o-resolve-ponteiros-arquivolinha-na-prosa",children:["O que o gerador N\xc3O resolve: ponteiros ",(0,n.jsx)(o.code,{children:"arquivo:linha"})," na prosa"]}),"\n",(0,n.jsxs)(o.p,{children:["Um ",(0,n.jsx)(o.code,{children:"game.js:5361"})," escrito no meio de um par\xe1grafo \xe9 a vers\xe3o barata do mesmo defeito \u2014 ele\naponta pro lugar errado no primeiro commit que mexer no arquivo. N\xe3o d\xe1 pra gerar (o\nponteiro faz parte da frase), mas d\xe1 pra ",(0,n.jsx)(o.strong,{children:"detectar o caso grosseiro"}),": ponteiro que aponta\npara al\xe9m do fim do arquivo."]}),"\n","\n",(0,n.jsxs)(o.p,{children:["Nenhum ponteiro ",(0,n.jsx)(o.code,{children:"arquivo:linha"})," das docs aponta para fora do arquivo que ele cita. \u2713"]}),"\n",(0,n.jsxs)(o.blockquote,{children:["\n",(0,n.jsxs)(o.p,{children:["Isto confere s\xf3 o ",(0,n.jsx)(o.strong,{children:"limite"})," do arquivo: um ponteiro que ainda cabe mas mudou de assunto passa aqui. \xc9 a raz\xe3o de a doutrina da casa ser declarar o S\xcdMBOLO e deixar a linha para o gerador \u2014 ver ",(0,n.jsx)(o.code,{children:"tools/gen-arch.mjs"}),"."]}),"\n"]}),"\n",(0,n.jsxs)(o.blockquote,{children:["\n",(0,n.jsxs)(o.p,{children:["Bloco gerado por ",(0,n.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(o.code,{children:"varredura de "}),"arquivo",":linha",(0,n.jsx)(o.code,{children:" em README/STATUS/HANDOFF/KNOWN-BUGS/docs/docs/SKILL"})]}),"\n"]}),"\n","\n",(0,n.jsxs)(o.p,{children:["Por isso a doutrina \xe9 declarar o ",(0,n.jsx)(o.strong,{children:"s\xedmbolo"})," e deixar a linha para o gerador. Quando o\n",(0,n.jsx)(o.code,{children:"arquivo:linha"})," for mesmo necess\xe1rio, cite junto o nome do que est\xe1 l\xe1 \u2014 assim quem ler\ndaqui a um m\xeas acha por ",(0,n.jsx)(o.code,{children:"grep"})," mesmo com o ponteiro deslocado."]})]})}function h(e={}){const{wrapper:o}={...(0,d.R)(),...e.components};return o?(0,n.jsx)(o,{...e,children:(0,n.jsx)(t,{...e})}):t(e)}},8453(e,o,s){s.d(o,{R:()=>a,x:()=>i});var r=s(6540);const n={},d=r.createContext(n);function a(e){const o=r.useContext(d);return r.useMemo(function(){return"function"==typeof e?e(o):{...o,...e}},[o,e])}function i(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),r.createElement(d.Provider,{value:o},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/assets/js/745ddde7.cbd3d5a7.js b/public/docs/assets/js/745ddde7.0263fb18.js similarity index 99% rename from public/docs/assets/js/745ddde7.cbd3d5a7.js rename to public/docs/assets/js/745ddde7.0263fb18.js index 619ec88a0..980e0dc46 100644 --- a/public/docs/assets/js/745ddde7.cbd3d5a7.js +++ b/public/docs/assets/js/745ddde7.0263fb18.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[889],{9541(e,s,o){o.r(s),o.d(s,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>l});const r=JSON.parse('{"id":"stack","title":"Stack e ferramentas","description":"Three.js/WebGL sem build, Astro na Vercel, Supabase, o pipeline de gera\xe7\xe3o de asset (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform e as skills de agente \u2014 cada um com a vers\xe3o lida do package.json.","source":"@site/docs/stack.md","sourceDirName":".","slug":"/stack","permalink":"/docs/stack","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/stack.md","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"id":"stack","title":"Stack e ferramentas","sidebar_label":"Stack e ferramentas","sidebar_position":2,"description":"Three.js/WebGL sem build, Astro na Vercel, Supabase, o pipeline de gera\xe7\xe3o de asset (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform e as skills de agente \u2014 cada um com a vers\xe3o lida do package.json."},"sidebar":"dev","previous":{"title":"Come\xe7ando","permalink":"/docs/"},"next":{"title":"Instrumenta\xe7\xe3o de IA","permalink":"/docs/instrumentacao-ai"}}');var n=o(4848),d=o(8453);const a={id:"stack",title:"Stack e ferramentas",sidebar_label:"Stack e ferramentas",sidebar_position:2,description:"Three.js/WebGL sem build, Astro na Vercel, Supabase, o pipeline de gera\xe7\xe3o de asset (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform e as skills de agente \u2014 cada um com a vers\xe3o lida do package.json."},i="Stack e ferramentas",c={},l=[{value:"As duas zonas, e por que a fronteira \xe9 dura",id:"as-duas-zonas-e-por-que-a-fronteira-\xe9-dura",level:2},{value:"public/ \u2014 o JOGO: Three.js, WebGL, zero build",id:"public--o-jogo-threejs-webgl-zero-build",level:3},{value:"src/ \u2014 o SITE: Astro com SSR na Vercel",id:"src--o-site-astro-com-ssr-na-vercel",level:3},{value:"Banco \u2014 Postgres gerenciado, RLS e telemetria",id:"banco--postgres-gerenciado-rls-e-telemetria",level:3},{value:"Gera\xe7\xe3o de asset \u2014 o que \xe9 gerado por IA, e por qual servi\xe7o",id:"gera\xe7\xe3o-de-asset--o-que-\xe9-gerado-por-ia-e-por-qual-servi\xe7o",level:2},{value:"Personagens: mint.gg",id:"personagens-mintgg",level:3},{value:"Props 3D: Tripo3D e Meshy",id:"props-3d-tripo3d-e-meshy",level:3},{value:"Arte 2D: OpenRouter",id:"arte-2d-openrouter",level:3},{value:"As chaves",id:"as-chaves",level:3},{value:"Otimiza\xe7\xe3o de GLB: gltf-transform e meshoptimizer",id:"otimiza\xe7\xe3o-de-glb-gltf-transform-e-meshoptimizer",level:2},{value:"Playwright \u2014 todo arn\xeas que precisa de browser",id:"playwright--todo-arn\xeas-que-precisa-de-browser",level:2},{value:"Skills de agente",id:"skills-de-agente",level:2},{value:"O gauntlet loop",id:"o-gauntlet-loop",level:3},{value:"A documenta\xe7\xe3o",id:"a-documenta\xe7\xe3o",level:2}];function t(e){const s={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(s.header,{children:(0,n.jsx)(s.h1,{id:"stack-e-ferramentas",children:"Stack e ferramentas"})}),"\n",(0,n.jsxs)(s.p,{children:["Esta p\xe1gina responde \xe0 pergunta ",(0,n.jsx)(s.em,{children:'"com o que isso \xe9 feito?"'})," \u2014 e responde com a ",(0,n.jsx)(s.strong,{children:"vers\xe3o\ndeclarada"}),", n\xe3o com a lembrada. A tabela abaixo \xe9 gerada por ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),"\na partir do ",(0,n.jsx)(s.code,{children:"package.json"}),", do ",(0,n.jsx)(s.code,{children:"docs/package.json"})," e do pr\xf3prio Three.js vendorizado."]}),"\n","\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Camada"}),(0,n.jsx)(s.th,{children:"Ferramenta"}),(0,n.jsx)(s.th,{children:"Vers\xe3o"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Motor 3D (WebGL)"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"Three.js"}),", vendorizado"]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"r160"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Jogo"}),(0,n.jsxs)(s.td,{children:["ES modules vanilla, ",(0,n.jsx)(s.strong,{children:"zero build"})]}),(0,n.jsx)(s.td,{children:"44 arquivos"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Site"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"Astro"})," com SSR"]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^7.1.1"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Hospedagem"}),(0,n.jsxs)(s.td,{children:["adapter ",(0,n.jsx)(s.strong,{children:"Vercel"})]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^11.0.3"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Banco"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"Postgres gerenciado"})," (RLS; schema privado, fora do repo)"]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^2.110.7"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Browser nas r\xe9guas"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Playwright"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^1.62.1"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Pipeline de GLB"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"gltf-transform"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^4.4.1"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Compress\xe3o de malha"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"meshoptimizer"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^1.2.0"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Imagem (build e API)"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"sharp"})," \xb7 ",(0,n.jsx)(s.strong,{children:"resvg"})]}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.code,{children:"^0.35.3"})," \xb7 ",(0,n.jsx)(s.code,{children:"^2.6.2"})]})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Esta documenta\xe7\xe3o"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Docusaurus"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"3.6.3"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Runtime de CI"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Node"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"22"})})]})]})]}),"\n",(0,n.jsxs)(s.p,{children:["Three.js sai de ",(0,n.jsx)(s.code,{children:"public/vendor/three.module.js"})," (",(0,n.jsx)(s.strong,{children:"sem CDN, sem npm no runtime"}),"). Astro e Vercel de ",(0,n.jsx)(s.code,{children:"package.json"})," + ",(0,n.jsx)(s.code,{children:"astro.config.mjs"})," + ",(0,n.jsx)(s.code,{children:"vercel.json"}),". Dos scripts de ",(0,n.jsx)(s.code,{children:"tools/"}),", ",(0,n.jsx)(s.strong,{children:"109"})," importam Playwright, ",(0,n.jsx)(s.strong,{children:"37"})," importam gltf-transform e ",(0,n.jsx)(s.strong,{children:"4"})," importam meshoptimizer."]}),"\n",(0,n.jsxs)(s.blockquote,{children:["\n",(0,n.jsxs)(s.p,{children:["Bloco gerado por ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(s.code,{children:"dependencies/devDependencies do package.json \xb7 REVISION de public/vendor/three.module.js"})]}),"\n"]}),"\n","\n",(0,n.jsx)(s.h2,{id:"as-duas-zonas-e-por-que-a-fronteira-\xe9-dura",children:"As duas zonas, e por que a fronteira \xe9 dura"}),"\n",(0,n.jsxs)(s.p,{children:["O reposit\xf3rio tem ",(0,n.jsx)(s.strong,{children:"duas aplica\xe7\xf5es com regras opostas"}),", e quase todo mal-entendido de\nquem chega nasce de trat\xe1-las como uma s\xf3."]}),"\n",(0,n.jsxs)(s.h3,{id:"public--o-jogo-threejs-webgl-zero-build",children:[(0,n.jsx)(s.code,{children:"public/"})," \u2014 o JOGO: Three.js, WebGL, zero build"]}),"\n",(0,n.jsxs)(s.p,{children:["O jogo \xe9 ",(0,n.jsx)(s.strong,{children:"JavaScript vanilla com ES modules servidos crus"}),". N\xe3o h\xe1 bundler, n\xe3o h\xe1\ntranspiler, n\xe3o h\xe1 passo de build. O browser baixa ",(0,n.jsx)(s.code,{children:"public/js/game.js"})," como est\xe1 no\nreposit\xf3rio."]}),"\n",(0,n.jsxs)(s.p,{children:["Isso \xe9 ",(0,n.jsx)(s.strong,{children:"decis\xe3o de projeto, n\xe3o pregui\xe7a"}),", e ela paga em tr\xeas lugares:"]}),"\n",(0,n.jsxs)(s.ol,{children:["\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"O jogo roda arrastando a pasta pra qualquer host est\xe1tico."})," N\xe3o depende do Astro,\nn\xe3o depende da Vercel, n\xe3o depende de npm no runtime. \xc9 o que torna vi\xe1vel entregar em\nportal (CrazyGames, itch) sem reescrever nada."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsxs)(s.strong,{children:["O arn\xeas consegue subir a classe ",(0,n.jsx)(s.code,{children:"Game"})," em node puro."]})," ",(0,n.jsx)(s.code,{children:"tools/eval/harness.mjs"}),"\nimporta o ",(0,n.jsx)(s.strong,{children:"c\xf3digo de produ\xe7\xe3o"})," com DOM e canvas stubados, e mede o jogo real em\nsegundos. Um bundler no meio quebraria isso \u2014 e sem isso n\xe3o existe quality gate."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsxs)(s.strong,{children:[(0,n.jsx)(s.code,{children:"node --check"})," em cada arquivo \xe9 um teste de sintaxe completo"]})," (",(0,n.jsx)(s.code,{children:"npm run syntax"}),"),\nporque o arquivo que o node parseia \xe9 byte a byte o que o browser executa."]}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:["O pre\xe7o, que tamb\xe9m \xe9 real: ",(0,n.jsx)(s.strong,{children:"cache"}),". Sem build n\xe3o h\xe1 hash no nome do arquivo, ent\xe3o a\ninvalida\xe7\xe3o \xe9 manual \u2014 o ",(0,n.jsx)(s.code,{children:"?v="})," do import map. A regra e o que ela j\xe1 custou est\xe3o em\n",(0,n.jsx)(s.a,{href:"/docs/#as-duas-zonas",children:"Come\xe7ando"}),", num lugar s\xf3."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"Three.js \xe9 vendorizado"})," em ",(0,n.jsx)(s.code,{children:"public/vendor/three.module.js"})," (mais ",(0,n.jsx)(s.code,{children:"vendor/addons/"}),").\nSem CDN e sem depend\xeancia de runtime: o import map aponta para o arquivo local. N\xe3o\nadicione CDN nem pacote de runtime sem abrir issue."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"WebGL \xe9 o alvo, e m\xe1quina fraca \xe9 requisito."})," Existe caminho ",(0,n.jsx)(s.code,{children:"quality: 'low'"})," sem\np\xf3s-processamento e kill-switch por querystring em toda mudan\xe7a arriscada (",(0,n.jsx)(s.code,{children:"?bloom=0"}),",\n",(0,n.jsx)(s.code,{children:"?ao=0"}),", ",(0,n.jsx)(s.code,{children:"?fxaa=0"}),", ",(0,n.jsx)(s.code,{children:"?water=0"}),"). Toda mudan\xe7a de gr\xe1fico que exija render extra tem que\ndeclarar o custo medido."]}),"\n",(0,n.jsxs)(s.h3,{id:"src--o-site-astro-com-ssr-na-vercel",children:[(0,n.jsx)(s.code,{children:"src/"})," \u2014 o SITE: Astro com SSR na Vercel"]}),"\n",(0,n.jsxs)(s.p,{children:["O site \xe9 ",(0,n.jsx)(s.a,{href:"https://astro.build",children:"Astro"})," com o adapter da Vercel. ",(0,n.jsx)(s.code,{children:"astro.config.mjs"})," est\xe1 em\n",(0,n.jsx)(s.code,{children:"output: 'static'"})," ",(0,n.jsx)(s.strong,{children:"com adapter"}),", e as rotas que precisam de servidor optam por\n",(0,n.jsx)(s.code,{children:"export const prerender = false"})," uma a uma \u2014 \xe9 o caso de ",(0,n.jsx)(s.code,{children:"/ranking"}),", ",(0,n.jsx)(s.code,{children:"/u/*"}),",\n",(0,n.jsx)(s.code,{children:"/sitemap.xml"})," e de todas as rotas ",(0,n.jsx)(s.code,{children:"/api/*"}),"."]}),"\n",(0,n.jsx)(s.p,{children:"Aqui framework \xe9 bem-vindo. As regras que valem:"}),"\n",(0,n.jsxs)(s.ul,{children:["\n",(0,n.jsxs)(s.li,{children:["a ",(0,n.jsx)(s.code,{children:"service_role"})," do Supabase vive ",(0,n.jsx)(s.strong,{children:"s\xf3 no servidor"})," e nunca chega ao browser;"]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.code,{children:"site"})," no ",(0,n.jsx)(s.code,{children:"astro.config.mjs"})," est\xe1 ",(0,n.jsxs)(s.strong,{children:["com ",(0,n.jsx)(s.code,{children:"www"})]}),", e todo canonical sai da\xed;"]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.code,{children:"vercel.json"})," carrega os headers de seguran\xe7a (CSP, HSTS, nosniff, Referrer-Policy,\nPermissions-Policy) e o cache de CDN."]}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:["E a pegadinha que custa a primeira hora de todo mundo: ",(0,n.jsxs)(s.strong,{children:[(0,n.jsx)(s.code,{children:"src/pages/index.astro"})," \xc9 o\njogo"]}),", servido na rota ",(0,n.jsx)(s.code,{children:"/"}),". N\xe3o existe ",(0,n.jsx)(s.code,{children:"public/index.html"}),"."]}),"\n",(0,n.jsx)(s.h3,{id:"banco--postgres-gerenciado-rls-e-telemetria",children:"Banco \u2014 Postgres gerenciado, RLS e telemetria"}),"\n",(0,n.jsxs)(s.p,{children:["O ranking e a telemetria vivem num Postgres gerenciado. Schema e migrations s\xe3o\nprivados (fora do repo \u2014 decis\xe3o de seguran\xe7a); o runtime s\xf3 usa as envs.\nofusca\xe7\xe3o opcional que foi entregue pronta e ",(0,n.jsx)(s.strong,{children:"deliberadamente n\xe3o aplicada"}),"."]}),"\n",(0,n.jsxs)(s.p,{children:["A seguran\xe7a n\xe3o vem de esconder a ",(0,n.jsx)(s.code,{children:"anon"})," key \u2014 ela \xe9 p\xfablica por design. Vem das\n",(0,n.jsx)(s.em,{children:"policies"}),", dos grants por coluna e do rate limit contado no Postgres\n(",(0,n.jsx)(s.code,{children:"src/lib/ratelimit.ts"})," + RPC ",(0,n.jsx)(s.code,{children:"rl_take"}),"), n\xe3o em mem\xf3ria de lambda."]}),"\n",(0,n.jsxs)(s.p,{children:["Identidade de jogador usa UID est\xe1vel para selecionar a conta e token para\nautenticar a sess\xe3o; nick \xe9 atributo de exibi\xe7\xe3o. Clientes e bancos antigos t\xeam\nfallback tempor\xe1rio por ",(0,n.jsx)(s.code,{children:"nick + token"}),", documentado em ",(0,n.jsx)(s.code,{children:"docs/seguranca.md"}),"."]}),"\n",(0,n.jsxs)(s.p,{children:["Hoje a ",(0,n.jsx)(s.code,{children:"anon"})," key ",(0,n.jsx)(s.strong,{children:"n\xe3o sai do servidor"}),": existia um ",(0,n.jsx)(s.code,{children:"GET /api/config"}),' que a entregava\nao browser "pro client ligar OAuth/storage", mas nenhum cliente chegou a usar, e a rota\nfoi removida (issue #41). Se OAuth entrar na mesa, ela volta \u2014 com rate limit.']}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"O ranking est\xe1 desligado hoje"})," (",(0,n.jsx)(s.code,{children:"RANKING_ON"})," em ",(0,n.jsx)(s.code,{children:"src/lib/site.ts"}),") e foi trocado por\ntelemetria an\xf4nima. \xc9 flag, n\xe3o remo\xe7\xe3o \u2014 detalhes em ",(0,n.jsx)(s.a,{href:"/docs/estado",children:"Estado atual"}),"."]}),"\n",(0,n.jsx)(s.admonition,{title:"Nada disso \xe9 obrigat\xf3rio pra rodar o jogo",type:"note",children:(0,n.jsxs)(s.p,{children:["Sem as vari\xe1veis do Supabase o site sobe igual: as rotas de ranking respondem\n",(0,n.jsx)(s.code,{children:"503 not_configured"})," e as p\xe1ginas mostram o aviso. O jogo em ",(0,n.jsx)(s.code,{children:"public/"})," ",(0,n.jsx)(s.strong,{children:"n\xe3o usa nenhuma\ndelas"}),". Ver ",(0,n.jsx)(s.code,{children:".env.example"}),"."]})}),"\n",(0,n.jsx)(s.h2,{id:"gera\xe7\xe3o-de-asset--o-que-\xe9-gerado-por-ia-e-por-qual-servi\xe7o",children:"Gera\xe7\xe3o de asset \u2014 o que \xe9 gerado por IA, e por qual servi\xe7o"}),"\n",(0,n.jsxs)(s.p,{children:["Quase todo asset 3D e 2D deste jogo \xe9 ",(0,n.jsx)(s.strong,{children:"gerado"}),", n\xe3o modelado \xe0 m\xe3o. O fluxo real, n\xe3o o\nhipot\xe9tico:"]}),"\n","\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Servi\xe7o"}),(0,n.jsx)(s.th,{children:"O que gera"}),(0,n.jsx)(s.th,{children:"Script"}),(0,n.jsx)(s.th,{children:"Chave"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"mint.gg"})," (Mint MCP)"]}),(0,n.jsx)(s.td,{children:"personagens rigados, packs, anima\xe7\xe3o"}),(0,n.jsxs)(s.td,{children:["ferramentas MCP; o registro do que foi gerado \xe9 ",(0,n.jsx)(s.code,{children:"mint-assets.json"})]}),(0,n.jsx)(s.td,{children:"conta do dono, via MCP"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Tripo3D"})}),(0,n.jsx)(s.td,{children:"props 3D por texto (padr\xe3o)"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"tools/gen-asset.mjs --provider tripo"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"TRIPO_API_KEY"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Meshy"})}),(0,n.jsx)(s.td,{children:"props 3D por texto (alternativa) e rig"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"tools/gen-asset.mjs --provider meshy"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"MESHY_API_KEY"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"OpenRouter"})}),(0,n.jsx)(s.td,{children:"arte 2D (cartaz de fac\xe7\xe3o, wallpaper, splash)"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"tools/gen-image.mjs"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"OPENROUTER_API_KEY"})})]})]})]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"mint-assets.json"})," registra ",(0,n.jsx)(s.strong,{children:"7 assets"})," gerados via Mint (3 ",(0,n.jsx)(s.code,{children:"mint-model"})," \xb7 4 ",(0,n.jsx)(s.code,{children:"mint-asset-pack"}),"), cada um com ",(0,n.jsx)(s.code,{children:"assetId"}),", ",(0,n.jsx)(s.code,{children:"chatUrl"})," e notas do que deu errado na tentativa anterior."]}),"\n",(0,n.jsxs)(s.p,{children:["As tr\xeas chaves de API vivem em ",(0,n.jsx)(s.code,{children:".env"})," na raiz \u2014 ",(0,n.jsxs)(s.strong,{children:["gitignored, modo 600, nunca em ",(0,n.jsx)(s.code,{children:"argv"})]})," (argv vaza no ",(0,n.jsx)(s.code,{children:"ps"})," de qualquer processo da m\xe1quina). Sem elas o jogo roda igual: o pipeline de gera\xe7\xe3o \xe9 offline, o resultado \xe9 que entra no reposit\xf3rio."]}),"\n",(0,n.jsxs)(s.blockquote,{children:["\n",(0,n.jsxs)(s.p,{children:["Bloco gerado por ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(s.code,{children:"git grep -l SDK -- tools/ | grep .mjs \xb7 mint-assets.json"})]}),"\n"]}),"\n","\n",(0,n.jsx)(s.h3,{id:"personagens-mintgg",children:"Personagens: mint.gg"}),"\n",(0,n.jsxs)(s.p,{children:["Os personagens jog\xe1veis s\xe3o GLB rigados gerados pelo ",(0,n.jsx)(s.strong,{children:"Mint"})," (mint.gg), pelas ferramentas\nMCP \u2014 ",(0,n.jsx)(s.code,{children:"start_model_generation"})," com ",(0,n.jsx)(s.code,{children:"riggable_character"})," em T-pose e m\xe3os vazias, depois\n",(0,n.jsx)(s.code,{children:"animate_generated_model"})," para sair com esqueleto."]}),"\n",(0,n.jsx)(s.p,{children:"Dois fatos n\xe3o \xf3bvios que economizam dinheiro e rodada:"}),"\n",(0,n.jsxs)(s.ul,{children:["\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"O modelo base do Mint n\xe3o vem rigado."})," O esqueleto s\xf3 aparece no passo de anima\xe7\xe3o.\nO caminho barato para um personagem novo \xe9: gerar a base \u2192 rigar com ",(0,n.jsx)(s.strong,{children:"um"})," clipe \u2192\nusar o ",(0,n.jsx)(s.code,{children:"rigged_character_glb"})," dele \u2192 reaproveitar os clipes compartilhados."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"Os rigs Meshy compartilham os mesmos nomes de osso"})," (",(0,n.jsx)(s.code,{children:"Hips"}),", ",(0,n.jsx)(s.code,{children:"Spine"}),", ",(0,n.jsx)(s.code,{children:"Head"}),",\n",(0,n.jsx)(s.code,{children:"RightHand"}),"\u2026), ent\xe3o um pack de clipes gerado uma vez casa por nome em qualquer rig da\nfam\xedlia. \xc9 por isso que ",(0,n.jsx)(s.code,{children:"public/models/anims/"})," tem clipe compartilhado e clipe pr\xf3prio\nao mesmo tempo, e por isso existe o manifesto ",(0,n.jsx)(s.code,{children:"index.json"})," (",(0,n.jsx)(s.code,{children:"npm run anims"}),") \u2014 sem ele o\njogo pedia clipe de quem n\xe3o tem e enchia o console de 404."]}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"mint-assets.json"})," \xe9 o registro do que foi gerado: ",(0,n.jsx)(s.code,{children:"assetId"}),", ",(0,n.jsx)(s.code,{children:"chatUrl"})," e uma nota do que\ndeu errado na tentativa anterior. ",(0,n.jsx)(s.strong,{children:"Sem esse registro n\xe3o d\xe1 para revisar nem regerar"})," \u2014\no asset vira um bin\xe1rio sem proced\xeancia no meio do reposit\xf3rio."]}),"\n",(0,n.jsx)(s.h3,{id:"props-3d-tripo3d-e-meshy",children:"Props 3D: Tripo3D e Meshy"}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"tools/gen-asset.mjs"})," gera um prop por texto, baixa o GLB e grava ",(0,n.jsx)(s.strong,{children:"j\xe1 otimizado"})," em\n",(0,n.jsx)(s.code,{children:"public/models/props/"}),":"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:'node tools/gen-asset.mjs --prompt "caixa de som de baile" --id caixa_som\nnode tools/gen-asset.mjs --provider meshy --prompt "carro tunado" --id carro_tunado\nnode tools/gen-asset.mjs --resume --id caixa_som # tarefa j\xe1 paga\n'})}),"\n",(0,n.jsxs)(s.p,{children:["Tripo \xe9 o padr\xe3o; Meshy \xe9 a alternativa. ",(0,n.jsx)(s.code,{children:"--face-limit"})," (padr\xe3o 12000), ",(0,n.jsx)(s.code,{children:"--raw-only"})," para\npular a otimiza\xe7\xe3o e ",(0,n.jsx)(s.code,{children:"--timeout"})," completam as op\xe7\xf5es."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"Mapa n\xe3o precisa disso."})," Os mapas registrados s\xe3o geometria procedural em Three.js \u2014\nrua, barraco, beco, cal\xe7ada e rotunda s\xe3o caixa e plano, que \xe9 o que ",(0,n.jsx)(s.code,{children:"map_*.js"})," j\xe1 faz. O\nque vem de GLB s\xe3o ",(0,n.jsx)(s.strong,{children:"props"}),"."]}),"\n",(0,n.jsx)(s.h3,{id:"arte-2d-openrouter",children:"Arte 2D: OpenRouter"}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"tools/gen-image.mjs"})," \xe9 o irm\xe3o 2D: gera cartaz de fac\xe7\xe3o, wallpaper e splash por texto\n(+ imagens de refer\xeancia), e entrega o arquivo ",(0,n.jsx)(s.strong,{children:"j\xe1 enquadrado e comprimido"})," para a caixa\nem que a tela vai desenh\xe1-lo."]}),"\n",(0,n.jsxs)(s.p,{children:["O recorte mora no script de prop\xf3sito. Placa de fac\xe7\xe3o \xe9 uma caixa ",(0,n.jsx)(s.code,{children:"245\xd7620"})," com\n",(0,n.jsx)(s.code,{children:"background-size: cover"}),"; arte em paisagem entra nela mostrando ~26% da largura \u2014 foi\nassim que quatro cartazes de elenco viraram quatro retratos de UM personagem. O gerador\nn\xe3o oferece essa propor\xe7\xe3o, ent\xe3o quem publica \xe9 quem fecha a conta: gera no aspecto mais\npr\xf3ximo e recorta pelo centro at\xe9 a propor\xe7\xe3o ",(0,n.jsx)(s.strong,{children:"real"})," da caixa. Assim o que se olha antes\nde commitar \xe9 byte a byte o que o jogador v\xea."]}),"\n",(0,n.jsx)(s.h3,{id:"as-chaves",children:"As chaves"}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"TRIPO_API_KEY"}),", ",(0,n.jsx)(s.code,{children:"MESHY_API_KEY"})," e ",(0,n.jsx)(s.code,{children:"OPENROUTER_API_KEY"})," s\xe3o lidas de um ",(0,n.jsx)(s.code,{children:".env"})," na raiz \u2014\n",(0,n.jsx)(s.strong,{children:"gitignored, modo 600"}),". Tr\xeas regras que os dois scripts compartilham, cada uma com\nmotivo:"]}),"\n",(0,n.jsxs)(s.ol,{children:["\n",(0,n.jsxs)(s.li,{children:[(0,n.jsxs)(s.strong,{children:["A chave nunca vem de ",(0,n.jsx)(s.code,{children:"argv"}),"."]})," Argumento de linha de comando vaza no ",(0,n.jsx)(s.code,{children:"ps"})," de qualquer\nprocesso da m\xe1quina."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsxs)(s.strong,{children:["O header ",(0,n.jsx)(s.code,{children:"Authorization"})," s\xf3 sai para o host da pr\xf3pria API."]})," O GLB pronto vem de um\nCDN de terceiro (link assinado); mandar a chave junto no download entregaria credencial\npara um host que n\xe3o \xe9 o do provedor. H\xe1 allowlist, e ",(0,n.jsx)(s.code,{children:"redirect: 'error'"})," impede que um\n3xx carregue o header para outro dom\xednio."]}),"\n",(0,n.jsx)(s.li,{children:(0,n.jsxs)(s.strong,{children:["Nada \xe9 impresso sem passar por ",(0,n.jsx)(s.code,{children:"redact()"}),"."]})}),"\n"]}),"\n",(0,n.jsxs)(s.admonition,{type:"caution",children:[(0,n.jsxs)(s.mdxAdmonitionTitle,{children:["Essas tr\xeas chaves n\xe3o est\xe3o no ",(0,n.jsx)(s.code,{children:".env.example"})]}),(0,n.jsxs)(s.p,{children:["O ",(0,n.jsx)(s.code,{children:".env.example"})," cobre s\xf3 Supabase e o pacote de \xe1udio. As chaves de gera\xe7\xe3o de asset\nexistem apenas no ",(0,n.jsx)(s.code,{children:".env"})," do dono. Quem clonar e quiser gerar asset precisa cri\xe1-las \xe0 m\xe3o\ncom os nomes acima \u2014 est\xe1 documentado aqui e no cabe\xe7alho de cada script, n\xe3o no exemplo."]})]}),"\n",(0,n.jsx)(s.h2,{id:"otimiza\xe7\xe3o-de-glb-gltf-transform-e-meshoptimizer",children:"Otimiza\xe7\xe3o de GLB: gltf-transform e meshoptimizer"}),"\n",(0,n.jsxs)(s.p,{children:["Todo GLB que entra no reposit\xf3rio passa por ",(0,n.jsx)(s.code,{children:"@gltf-transform"})," (",(0,n.jsx)(s.code,{children:"dedup"}),", ",(0,n.jsx)(s.code,{children:"prune"}),",\n",(0,n.jsx)(s.code,{children:"textureCompress"})," com ",(0,n.jsx)(s.strong,{children:"sharp"})," para WebP) e, no caminho est\xe1tico, por ",(0,n.jsx)(s.strong,{children:"meshoptimizer"}),"."]}),"\n",(0,n.jsxs)(s.p,{children:["O motivo \xe9 um teto real: ",(0,n.jsx)(s.strong,{children:"250 MB na CrazyGames"}),". GLB cru de personagem chega em 4-5 MB,\ndominado por textura PNG 2K \u2014 e a otimiza\xe7\xe3o \xe9 quase toda de textura, n\xe3o de malha."]}),"\n",(0,n.jsxs)(s.p,{children:["Os scripts de pipeline vivem em ",(0,n.jsx)(s.code,{children:"tools/"}),": ",(0,n.jsx)(s.code,{children:"optimize-props.mjs"}),", ",(0,n.jsx)(s.code,{children:"optimize-static.mjs"}),",\n",(0,n.jsx)(s.code,{children:"optimize-fpvm.mjs"}),", ",(0,n.jsx)(s.code,{children:"optimize-tribos.mjs"}),", mais os de rig (",(0,n.jsx)(s.code,{children:"rig-from-donor.mjs"}),",\n",(0,n.jsx)(s.code,{children:"reskin-glb.mjs"}),", ",(0,n.jsx)(s.code,{children:"retarget-glb.mjs"}),") e os de inspe\xe7\xe3o (",(0,n.jsx)(s.code,{children:"inspect-glb.mjs"}),",\n",(0,n.jsx)(s.code,{children:"inspect-anim.mjs"}),", ",(0,n.jsx)(s.code,{children:"bones.mjs"}),")."]}),"\n",(0,n.jsx)(s.h2,{id:"playwright--todo-arn\xeas-que-precisa-de-browser",children:"Playwright \u2014 todo arn\xeas que precisa de browser"}),"\n",(0,n.jsxs)(s.p,{children:["R\xe9gua que depende de ",(0,n.jsx)(s.strong,{children:"pixel"})," roda em Chromium via Playwright. \xc9 o caso de\n",(0,n.jsx)(s.code,{children:"tools/eval/*-capture.mjs"}),", ",(0,n.jsx)(s.code,{children:"telas-*.mjs"}),", ",(0,n.jsx)(s.code,{children:"select-inflate.mjs"}),", ",(0,n.jsx)(s.code,{children:"crash-watch.mjs"})," e\n",(0,n.jsx)(s.code,{children:"fv-verify.mjs"}),", entre outros."]}),"\n",(0,n.jsx)(s.p,{children:"Duas coisas que voc\xea precisa saber antes de rodar qualquer um:"}),"\n",(0,n.jsxs)(s.ul,{children:["\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"Custa caro."})," Render por software (SwiftShader) roda o jogo a ~0,3 FPS; uma captura\nin-game custa minutos por mapa/aspecto. Foi exatamente esse custo que empurrou o quality gate\npara node puro \u2014 e \xe9 por isso que as invariantes de pixel (",(0,n.jsx)(s.code,{children:"PX1"}),"\u2013",(0,n.jsx)(s.code,{children:"PX4"}),") est\xe3o\n",(0,n.jsx)(s.strong,{children:"puladas"}),", com o motivo dito."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"Uma sess\xe3o por vez."}),' Duas capturas headless em paralelo derrubam o boot e produzem\n"countdown travado" que parece bug e \xe9 carga. Um \xfanico agente roda browser.']}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:["Alguns arn\xeases precisam do servidor no ar: ",(0,n.jsx)(s.code,{children:"npm run eval:serve &"})," antes."]}),"\n",(0,n.jsx)(s.h2,{id:"skills-de-agente",children:"Skills de agente"}),"\n",(0,n.jsxs)(s.p,{children:["Este reposit\xf3rio versiona ",(0,n.jsx)(s.strong,{children:"skills"})," \u2014 instru\xe7\xf5es empacotadas que um agente carrega antes\nde trabalhar. Elas vivem em ",(0,n.jsx)(s.code,{children:".agents/skills/"}),", e ",(0,n.jsx)(s.code,{children:".claude/skills/"})," s\xe3o symlinks para l\xe1."]}),"\n","\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Contagem"}),(0,n.jsx)(s.th,{style:{textAlign:"right"},children:"Quanto"}),(0,n.jsx)(s.th,{children:"O que significa"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:["Declaradas no ",(0,n.jsx)(s.code,{children:"skills-lock.json"})]}),(0,n.jsx)(s.td,{style:{textAlign:"right"},children:"39"}),(0,n.jsxs)(s.td,{children:["com ",(0,n.jsx)(s.code,{children:"source"}),", ",(0,n.jsx)(s.code,{children:"skillPath"})," e ",(0,n.jsx)(s.code,{children:"computedHash"})," \u2014 skill de terceiro que mudar de conte\xfado \xe9 detect\xe1vel"]})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Versionadas (chegam em quem clona)"}),(0,n.jsx)(s.td,{style:{textAlign:"right"},children:"10"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"git ls-files .agents/skills"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:["\u2026dessas, com ",(0,n.jsx)(s.code,{children:"SKILL.md"})," no git"]}),(0,n.jsx)(s.td,{style:{textAlign:"right"},children:"10"}),(0,n.jsx)(s.td,{children:"\xe9 o que um clone limpo consegue ler"})]})]})]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"As contagens divergem de prop\xf3sito, e a diferen\xe7a \xe9 o fato:"})," a maioria das skills \xe9 de terceiro, fixada por hash no lock e baixada sob demanda. Quem clonar o reposit\xf3rio recebe o lock inteiro e s\xf3 uma parte do conte\xfado. Publicar s\xf3 uma das contagens esconderia exatamente o que o contribuidor precisa saber."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:".claude/skills/"})," s\xe3o ",(0,n.jsx)(s.strong,{children:"symlinks"})," para ",(0,n.jsx)(s.code,{children:".agents/skills/"})," \u2014 uma c\xf3pia s\xf3, dois nomes, porque o Claude Code l\xea de ",(0,n.jsx)(s.code,{children:".claude/"})," e outros arn\xeases leem de ",(0,n.jsx)(s.code,{children:".agents/"}),"."]}),"\n",(0,n.jsxs)(s.p,{children:["A skill do loop desta casa, ",(0,n.jsx)(s.strong,{children:(0,n.jsx)(s.code,{children:"gauntlet-fps"})}),", \xe9 a \xfanica que nasceu aqui: vive em ",(0,n.jsx)(s.code,{children:".claude/skills/gauntlet-fps/SKILL.md"}),", n\xe3o \xe9 symlink e n\xe3o entra no lock."]}),"\n",(0,n.jsxs)(s.blockquote,{children:["\n",(0,n.jsxs)(s.p,{children:["Bloco gerado por ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(s.code,{children:"git ls-files .agents/skills \xb7 skills-lock.json"})]}),"\n"]}),"\n","\n",(0,n.jsx)(s.p,{children:"A grande maioria \xe9 de terceiro e cobre Three.js (materiais, ilumina\xe7\xe3o, shaders,\np\xf3s-processamento, carregamento de glTF, geometria, anima\xe7\xe3o) e game design. Elas s\xe3o\ncontexto opcional: nada no jogo depende delas."}),"\n",(0,n.jsx)(s.h3,{id:"o-gauntlet-loop",children:"O gauntlet loop"}),"\n",(0,n.jsxs)(s.p,{children:["A skill que ",(0,n.jsx)(s.strong,{children:"n\xe3o"})," \xe9 de terceiro \xe9 a ",(0,n.jsx)(s.code,{children:"gauntlet-fps"}),", e ela codifica o ciclo de trabalho\ndesta casa:"]}),"\n",(0,n.jsxs)(s.blockquote,{children:["\n",(0,n.jsx)(s.p,{children:(0,n.jsx)(s.strong,{children:"cr\xedtico adversarial \u2192 construtores em paralelo \u2192 captura medida \u2192 verifica\xe7\xe3o A/B \u2192\nca\xe7ador de regress\xf5es"})}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"Quando usar:"}),' melhorar, avaliar ou revisar qualquer parte do jogo \u2014 gr\xe1ficos,\nfidelidade de mapa, feel de arma, menu, HUD, bots, movimento \u2014 e quando algo \xe9 reportado\ncomo feio, estranho ou "n\xe3o parece profissional". ',(0,n.jsx)(s.strong,{children:"Quando n\xe3o usar:"})," tarefa mec\xe2nica de\numa linha, ou pergunta conceitual que n\xe3o mexe no jogo."]}),"\n",(0,n.jsxs)(s.p,{children:["O ciclo inteiro \u2014 as tr\xeas regras, o problema que cada uma resolve e o caso medido de cada\numa \u2014 tem p\xe1gina pr\xf3pria: ",(0,n.jsx)(s.strong,{children:(0,n.jsx)(s.a,{href:"/docs/instrumentacao-ai",children:"Instrumenta\xe7\xe3o de IA"})}),". Esta se\xe7\xe3o\nexiste s\xf3 para dizer que a skill existe e quando acion\xe1-la."]}),"\n",(0,n.jsx)(s.h2,{id:"a-documenta\xe7\xe3o",children:"A documenta\xe7\xe3o"}),"\n",(0,n.jsxs)(s.p,{children:["Esta doc \xe9 um ",(0,n.jsx)(s.strong,{children:"Docusaurus separado"}),", em ",(0,n.jsx)(s.code,{children:"docs/"}),", com o pr\xf3prio ",(0,n.jsx)(s.code,{children:"package.json"})," e o\npr\xf3prio ",(0,n.jsx)(s.code,{children:"node_modules"}),". Nada aqui \xe9 importado pelo jogo nem pelo site."]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:"cd docs && npm install && npm start # http://localhost:3000/docs/\ncd docs && npm run build # docs/build/\ncd docs && npm run build:site # buildar PARA DENTRO de public/docs/\n"})}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"baseUrl"})," \xe9 ",(0,n.jsx)(s.code,{children:"/docs/"})," porque a sa\xedda pode ser buildada para ",(0,n.jsx)(s.code,{children:"public/docs/"}),", e o Astro copia\n",(0,n.jsx)(s.code,{children:"public/"})," inteiro para ",(0,n.jsx)(s.code,{children:"dist/client/"}),"."]}),"\n",(0,n.jsx)(s.admonition,{title:"Todo n\xfamero desta p\xe1gina \xe9 gerado",type:"tip",children:(0,n.jsxs)(s.p,{children:["As tabelas acima saem de ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"})," e s\xe3o conferidas por\n",(0,n.jsx)(s.code,{children:"npm run docs:check"}),", dentro do ",(0,n.jsx)(s.code,{children:"check:fast"}),". O mecanismo \u2014 o que entra num bloco gerado, o\nque fica escrito \xe0 m\xe3o, e como colar um bloco novo \u2014 est\xe1 em\n",(0,n.jsx)(s.a,{href:"/docs/arquitetura#o-que-%C3%A9-gerado-e-o-que-n%C3%A3o-%C3%A9",children:"Arquitetura"}),"."]})})]})}function h(e={}){const{wrapper:s}={...(0,d.R)(),...e.components};return s?(0,n.jsx)(s,{...e,children:(0,n.jsx)(t,{...e})}):t(e)}},8453(e,s,o){o.d(s,{R:()=>a,x:()=>i});var r=o(6540);const n={},d=r.createContext(n);function a(e){const s=r.useContext(d);return r.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function i(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),r.createElement(d.Provider,{value:s},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[889],{9541(e,s,o){o.r(s),o.d(s,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>l});const r=JSON.parse('{"id":"stack","title":"Stack e ferramentas","description":"Three.js/WebGL sem build, Astro na Vercel, Supabase, o pipeline de gera\xe7\xe3o de asset (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform e as skills de agente \u2014 cada um com a vers\xe3o lida do package.json.","source":"@site/docs/stack.md","sourceDirName":".","slug":"/stack","permalink":"/docs/stack","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/stack.md","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"id":"stack","title":"Stack e ferramentas","sidebar_label":"Stack e ferramentas","sidebar_position":2,"description":"Three.js/WebGL sem build, Astro na Vercel, Supabase, o pipeline de gera\xe7\xe3o de asset (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform e as skills de agente \u2014 cada um com a vers\xe3o lida do package.json."},"sidebar":"dev","previous":{"title":"Come\xe7ando","permalink":"/docs/"},"next":{"title":"Instrumenta\xe7\xe3o de IA","permalink":"/docs/instrumentacao-ai"}}');var n=o(4848),d=o(8453);const a={id:"stack",title:"Stack e ferramentas",sidebar_label:"Stack e ferramentas",sidebar_position:2,description:"Three.js/WebGL sem build, Astro na Vercel, Supabase, o pipeline de gera\xe7\xe3o de asset (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform e as skills de agente \u2014 cada um com a vers\xe3o lida do package.json."},i="Stack e ferramentas",c={},l=[{value:"As duas zonas, e por que a fronteira \xe9 dura",id:"as-duas-zonas-e-por-que-a-fronteira-\xe9-dura",level:2},{value:"public/ \u2014 o JOGO: Three.js, WebGL, zero build",id:"public--o-jogo-threejs-webgl-zero-build",level:3},{value:"src/ \u2014 o SITE: Astro com SSR na Vercel",id:"src--o-site-astro-com-ssr-na-vercel",level:3},{value:"Banco \u2014 Postgres gerenciado, RLS e telemetria",id:"banco--postgres-gerenciado-rls-e-telemetria",level:3},{value:"Gera\xe7\xe3o de asset \u2014 o que \xe9 gerado por IA, e por qual servi\xe7o",id:"gera\xe7\xe3o-de-asset--o-que-\xe9-gerado-por-ia-e-por-qual-servi\xe7o",level:2},{value:"Personagens: mint.gg",id:"personagens-mintgg",level:3},{value:"Props 3D: Tripo3D e Meshy",id:"props-3d-tripo3d-e-meshy",level:3},{value:"Arte 2D: OpenRouter",id:"arte-2d-openrouter",level:3},{value:"As chaves",id:"as-chaves",level:3},{value:"Otimiza\xe7\xe3o de GLB: gltf-transform e meshoptimizer",id:"otimiza\xe7\xe3o-de-glb-gltf-transform-e-meshoptimizer",level:2},{value:"Playwright \u2014 todo arn\xeas que precisa de browser",id:"playwright--todo-arn\xeas-que-precisa-de-browser",level:2},{value:"Skills de agente",id:"skills-de-agente",level:2},{value:"O gauntlet loop",id:"o-gauntlet-loop",level:3},{value:"A documenta\xe7\xe3o",id:"a-documenta\xe7\xe3o",level:2}];function t(e){const s={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(s.header,{children:(0,n.jsx)(s.h1,{id:"stack-e-ferramentas",children:"Stack e ferramentas"})}),"\n",(0,n.jsxs)(s.p,{children:["Esta p\xe1gina responde \xe0 pergunta ",(0,n.jsx)(s.em,{children:'"com o que isso \xe9 feito?"'})," \u2014 e responde com a ",(0,n.jsx)(s.strong,{children:"vers\xe3o\ndeclarada"}),", n\xe3o com a lembrada. A tabela abaixo \xe9 gerada por ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),"\na partir do ",(0,n.jsx)(s.code,{children:"package.json"}),", do ",(0,n.jsx)(s.code,{children:"docs/package.json"})," e do pr\xf3prio Three.js vendorizado."]}),"\n","\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Camada"}),(0,n.jsx)(s.th,{children:"Ferramenta"}),(0,n.jsx)(s.th,{children:"Vers\xe3o"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Motor 3D (WebGL)"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"Three.js"}),", vendorizado"]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"r160"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Jogo"}),(0,n.jsxs)(s.td,{children:["ES modules vanilla, ",(0,n.jsx)(s.strong,{children:"zero build"})]}),(0,n.jsx)(s.td,{children:"44 arquivos"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Site"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"Astro"})," com SSR"]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^7.1.1"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Hospedagem"}),(0,n.jsxs)(s.td,{children:["adapter ",(0,n.jsx)(s.strong,{children:"Vercel"})]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^11.0.6"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Banco"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"Postgres gerenciado"})," (RLS; schema privado, fora do repo)"]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^2.110.7"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Browser nas r\xe9guas"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Playwright"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^1.62.1"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Pipeline de GLB"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"gltf-transform"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^4.4.1"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Compress\xe3o de malha"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"meshoptimizer"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"^1.2.0"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Imagem (build e API)"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"sharp"})," \xb7 ",(0,n.jsx)(s.strong,{children:"resvg"})]}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.code,{children:"^0.35.3"})," \xb7 ",(0,n.jsx)(s.code,{children:"^2.6.2"})]})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Esta documenta\xe7\xe3o"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Docusaurus"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"3.6.3"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Runtime de CI"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Node"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"22"})})]})]})]}),"\n",(0,n.jsxs)(s.p,{children:["Three.js sai de ",(0,n.jsx)(s.code,{children:"public/vendor/three.module.js"})," (",(0,n.jsx)(s.strong,{children:"sem CDN, sem npm no runtime"}),"). Astro e Vercel de ",(0,n.jsx)(s.code,{children:"package.json"})," + ",(0,n.jsx)(s.code,{children:"astro.config.mjs"})," + ",(0,n.jsx)(s.code,{children:"vercel.json"}),". Dos scripts de ",(0,n.jsx)(s.code,{children:"tools/"}),", ",(0,n.jsx)(s.strong,{children:"110"})," importam Playwright, ",(0,n.jsx)(s.strong,{children:"37"})," importam gltf-transform e ",(0,n.jsx)(s.strong,{children:"4"})," importam meshoptimizer."]}),"\n",(0,n.jsxs)(s.blockquote,{children:["\n",(0,n.jsxs)(s.p,{children:["Bloco gerado por ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(s.code,{children:"dependencies/devDependencies do package.json \xb7 REVISION de public/vendor/three.module.js"})]}),"\n"]}),"\n","\n",(0,n.jsx)(s.h2,{id:"as-duas-zonas-e-por-que-a-fronteira-\xe9-dura",children:"As duas zonas, e por que a fronteira \xe9 dura"}),"\n",(0,n.jsxs)(s.p,{children:["O reposit\xf3rio tem ",(0,n.jsx)(s.strong,{children:"duas aplica\xe7\xf5es com regras opostas"}),", e quase todo mal-entendido de\nquem chega nasce de trat\xe1-las como uma s\xf3."]}),"\n",(0,n.jsxs)(s.h3,{id:"public--o-jogo-threejs-webgl-zero-build",children:[(0,n.jsx)(s.code,{children:"public/"})," \u2014 o JOGO: Three.js, WebGL, zero build"]}),"\n",(0,n.jsxs)(s.p,{children:["O jogo \xe9 ",(0,n.jsx)(s.strong,{children:"JavaScript vanilla com ES modules servidos crus"}),". N\xe3o h\xe1 bundler, n\xe3o h\xe1\ntranspiler, n\xe3o h\xe1 passo de build. O browser baixa ",(0,n.jsx)(s.code,{children:"public/js/game.js"})," como est\xe1 no\nreposit\xf3rio."]}),"\n",(0,n.jsxs)(s.p,{children:["Isso \xe9 ",(0,n.jsx)(s.strong,{children:"decis\xe3o de projeto, n\xe3o pregui\xe7a"}),", e ela paga em tr\xeas lugares:"]}),"\n",(0,n.jsxs)(s.ol,{children:["\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"O jogo roda arrastando a pasta pra qualquer host est\xe1tico."})," N\xe3o depende do Astro,\nn\xe3o depende da Vercel, n\xe3o depende de npm no runtime. \xc9 o que torna vi\xe1vel entregar em\nportal (CrazyGames, itch) sem reescrever nada."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsxs)(s.strong,{children:["O arn\xeas consegue subir a classe ",(0,n.jsx)(s.code,{children:"Game"})," em node puro."]})," ",(0,n.jsx)(s.code,{children:"tools/eval/harness.mjs"}),"\nimporta o ",(0,n.jsx)(s.strong,{children:"c\xf3digo de produ\xe7\xe3o"})," com DOM e canvas stubados, e mede o jogo real em\nsegundos. Um bundler no meio quebraria isso \u2014 e sem isso n\xe3o existe quality gate."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsxs)(s.strong,{children:[(0,n.jsx)(s.code,{children:"node --check"})," em cada arquivo \xe9 um teste de sintaxe completo"]})," (",(0,n.jsx)(s.code,{children:"npm run syntax"}),"),\nporque o arquivo que o node parseia \xe9 byte a byte o que o browser executa."]}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:["O pre\xe7o, que tamb\xe9m \xe9 real: ",(0,n.jsx)(s.strong,{children:"cache"}),". Sem build n\xe3o h\xe1 hash no nome do arquivo, ent\xe3o a\ninvalida\xe7\xe3o \xe9 manual \u2014 o ",(0,n.jsx)(s.code,{children:"?v="})," do import map. A regra e o que ela j\xe1 custou est\xe3o em\n",(0,n.jsx)(s.a,{href:"/docs/#as-duas-zonas",children:"Come\xe7ando"}),", num lugar s\xf3."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"Three.js \xe9 vendorizado"})," em ",(0,n.jsx)(s.code,{children:"public/vendor/three.module.js"})," (mais ",(0,n.jsx)(s.code,{children:"vendor/addons/"}),").\nSem CDN e sem depend\xeancia de runtime: o import map aponta para o arquivo local. N\xe3o\nadicione CDN nem pacote de runtime sem abrir issue."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"WebGL \xe9 o alvo, e m\xe1quina fraca \xe9 requisito."})," Existe caminho ",(0,n.jsx)(s.code,{children:"quality: 'low'"})," sem\np\xf3s-processamento e kill-switch por querystring em toda mudan\xe7a arriscada (",(0,n.jsx)(s.code,{children:"?bloom=0"}),",\n",(0,n.jsx)(s.code,{children:"?ao=0"}),", ",(0,n.jsx)(s.code,{children:"?fxaa=0"}),", ",(0,n.jsx)(s.code,{children:"?water=0"}),"). Toda mudan\xe7a de gr\xe1fico que exija render extra tem que\ndeclarar o custo medido."]}),"\n",(0,n.jsxs)(s.h3,{id:"src--o-site-astro-com-ssr-na-vercel",children:[(0,n.jsx)(s.code,{children:"src/"})," \u2014 o SITE: Astro com SSR na Vercel"]}),"\n",(0,n.jsxs)(s.p,{children:["O site \xe9 ",(0,n.jsx)(s.a,{href:"https://astro.build",children:"Astro"})," com o adapter da Vercel. ",(0,n.jsx)(s.code,{children:"astro.config.mjs"})," est\xe1 em\n",(0,n.jsx)(s.code,{children:"output: 'static'"})," ",(0,n.jsx)(s.strong,{children:"com adapter"}),", e as rotas que precisam de servidor optam por\n",(0,n.jsx)(s.code,{children:"export const prerender = false"})," uma a uma \u2014 \xe9 o caso de ",(0,n.jsx)(s.code,{children:"/ranking"}),", ",(0,n.jsx)(s.code,{children:"/u/*"}),",\n",(0,n.jsx)(s.code,{children:"/sitemap.xml"})," e de todas as rotas ",(0,n.jsx)(s.code,{children:"/api/*"}),"."]}),"\n",(0,n.jsx)(s.p,{children:"Aqui framework \xe9 bem-vindo. As regras que valem:"}),"\n",(0,n.jsxs)(s.ul,{children:["\n",(0,n.jsxs)(s.li,{children:["a ",(0,n.jsx)(s.code,{children:"service_role"})," do Supabase vive ",(0,n.jsx)(s.strong,{children:"s\xf3 no servidor"})," e nunca chega ao browser;"]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.code,{children:"site"})," no ",(0,n.jsx)(s.code,{children:"astro.config.mjs"})," est\xe1 ",(0,n.jsxs)(s.strong,{children:["com ",(0,n.jsx)(s.code,{children:"www"})]}),", e todo canonical sai da\xed;"]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.code,{children:"vercel.json"})," carrega os headers de seguran\xe7a (CSP, HSTS, nosniff, Referrer-Policy,\nPermissions-Policy) e o cache de CDN."]}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:["E a pegadinha que custa a primeira hora de todo mundo: ",(0,n.jsxs)(s.strong,{children:[(0,n.jsx)(s.code,{children:"src/pages/index.astro"})," \xc9 o\njogo"]}),", servido na rota ",(0,n.jsx)(s.code,{children:"/"}),". N\xe3o existe ",(0,n.jsx)(s.code,{children:"public/index.html"}),"."]}),"\n",(0,n.jsx)(s.h3,{id:"banco--postgres-gerenciado-rls-e-telemetria",children:"Banco \u2014 Postgres gerenciado, RLS e telemetria"}),"\n",(0,n.jsxs)(s.p,{children:["O ranking e a telemetria vivem num Postgres gerenciado. Schema e migrations s\xe3o\nprivados (fora do repo \u2014 decis\xe3o de seguran\xe7a); o runtime s\xf3 usa as envs.\nofusca\xe7\xe3o opcional que foi entregue pronta e ",(0,n.jsx)(s.strong,{children:"deliberadamente n\xe3o aplicada"}),"."]}),"\n",(0,n.jsxs)(s.p,{children:["A seguran\xe7a n\xe3o vem de esconder a ",(0,n.jsx)(s.code,{children:"anon"})," key \u2014 ela \xe9 p\xfablica por design. Vem das\n",(0,n.jsx)(s.em,{children:"policies"}),", dos grants por coluna e do rate limit contado no Postgres\n(",(0,n.jsx)(s.code,{children:"src/lib/ratelimit.ts"})," + RPC ",(0,n.jsx)(s.code,{children:"rl_take"}),"), n\xe3o em mem\xf3ria de lambda."]}),"\n",(0,n.jsxs)(s.p,{children:["Identidade de jogador usa UID est\xe1vel para selecionar a conta e token para\nautenticar a sess\xe3o; nick \xe9 atributo de exibi\xe7\xe3o. Clientes e bancos antigos t\xeam\nfallback tempor\xe1rio por ",(0,n.jsx)(s.code,{children:"nick + token"}),", documentado em ",(0,n.jsx)(s.code,{children:"docs/seguranca.md"}),"."]}),"\n",(0,n.jsxs)(s.p,{children:["Hoje a ",(0,n.jsx)(s.code,{children:"anon"})," key ",(0,n.jsx)(s.strong,{children:"n\xe3o sai do servidor"}),": existia um ",(0,n.jsx)(s.code,{children:"GET /api/config"}),' que a entregava\nao browser "pro client ligar OAuth/storage", mas nenhum cliente chegou a usar, e a rota\nfoi removida (issue #41). Se OAuth entrar na mesa, ela volta \u2014 com rate limit.']}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"O ranking est\xe1 desligado hoje"})," (",(0,n.jsx)(s.code,{children:"RANKING_ON"})," em ",(0,n.jsx)(s.code,{children:"src/lib/site.ts"}),") e foi trocado por\ntelemetria an\xf4nima. \xc9 flag, n\xe3o remo\xe7\xe3o \u2014 detalhes em ",(0,n.jsx)(s.a,{href:"/docs/estado",children:"Estado atual"}),"."]}),"\n",(0,n.jsx)(s.admonition,{title:"Nada disso \xe9 obrigat\xf3rio pra rodar o jogo",type:"note",children:(0,n.jsxs)(s.p,{children:["Sem as vari\xe1veis do Supabase o site sobe igual: as rotas de ranking respondem\n",(0,n.jsx)(s.code,{children:"503 not_configured"})," e as p\xe1ginas mostram o aviso. O jogo em ",(0,n.jsx)(s.code,{children:"public/"})," ",(0,n.jsx)(s.strong,{children:"n\xe3o usa nenhuma\ndelas"}),". Ver ",(0,n.jsx)(s.code,{children:".env.example"}),"."]})}),"\n",(0,n.jsx)(s.h2,{id:"gera\xe7\xe3o-de-asset--o-que-\xe9-gerado-por-ia-e-por-qual-servi\xe7o",children:"Gera\xe7\xe3o de asset \u2014 o que \xe9 gerado por IA, e por qual servi\xe7o"}),"\n",(0,n.jsxs)(s.p,{children:["Quase todo asset 3D e 2D deste jogo \xe9 ",(0,n.jsx)(s.strong,{children:"gerado"}),", n\xe3o modelado \xe0 m\xe3o. O fluxo real, n\xe3o o\nhipot\xe9tico:"]}),"\n","\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Servi\xe7o"}),(0,n.jsx)(s.th,{children:"O que gera"}),(0,n.jsx)(s.th,{children:"Script"}),(0,n.jsx)(s.th,{children:"Chave"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.strong,{children:"mint.gg"})," (Mint MCP)"]}),(0,n.jsx)(s.td,{children:"personagens rigados, packs, anima\xe7\xe3o"}),(0,n.jsxs)(s.td,{children:["ferramentas MCP; o registro do que foi gerado \xe9 ",(0,n.jsx)(s.code,{children:"mint-assets.json"})]}),(0,n.jsx)(s.td,{children:"conta do dono, via MCP"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Tripo3D"})}),(0,n.jsx)(s.td,{children:"props 3D por texto (padr\xe3o)"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"tools/gen-asset.mjs --provider tripo"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"TRIPO_API_KEY"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"Meshy"})}),(0,n.jsx)(s.td,{children:"props 3D por texto (alternativa) e rig"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"tools/gen-asset.mjs --provider meshy"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"MESHY_API_KEY"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:(0,n.jsx)(s.strong,{children:"OpenRouter"})}),(0,n.jsx)(s.td,{children:"arte 2D (cartaz de fac\xe7\xe3o, wallpaper, splash)"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"tools/gen-image.mjs"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"OPENROUTER_API_KEY"})})]})]})]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"mint-assets.json"})," registra ",(0,n.jsx)(s.strong,{children:"7 assets"})," gerados via Mint (3 ",(0,n.jsx)(s.code,{children:"mint-model"})," \xb7 4 ",(0,n.jsx)(s.code,{children:"mint-asset-pack"}),"), cada um com ",(0,n.jsx)(s.code,{children:"assetId"}),", ",(0,n.jsx)(s.code,{children:"chatUrl"})," e notas do que deu errado na tentativa anterior."]}),"\n",(0,n.jsxs)(s.p,{children:["As tr\xeas chaves de API vivem em ",(0,n.jsx)(s.code,{children:".env"})," na raiz \u2014 ",(0,n.jsxs)(s.strong,{children:["gitignored, modo 600, nunca em ",(0,n.jsx)(s.code,{children:"argv"})]})," (argv vaza no ",(0,n.jsx)(s.code,{children:"ps"})," de qualquer processo da m\xe1quina). Sem elas o jogo roda igual: o pipeline de gera\xe7\xe3o \xe9 offline, o resultado \xe9 que entra no reposit\xf3rio."]}),"\n",(0,n.jsxs)(s.blockquote,{children:["\n",(0,n.jsxs)(s.p,{children:["Bloco gerado por ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(s.code,{children:"git grep -l SDK -- tools/ | grep .mjs \xb7 mint-assets.json"})]}),"\n"]}),"\n","\n",(0,n.jsx)(s.h3,{id:"personagens-mintgg",children:"Personagens: mint.gg"}),"\n",(0,n.jsxs)(s.p,{children:["Os personagens jog\xe1veis s\xe3o GLB rigados gerados pelo ",(0,n.jsx)(s.strong,{children:"Mint"})," (mint.gg), pelas ferramentas\nMCP \u2014 ",(0,n.jsx)(s.code,{children:"start_model_generation"})," com ",(0,n.jsx)(s.code,{children:"riggable_character"})," em T-pose e m\xe3os vazias, depois\n",(0,n.jsx)(s.code,{children:"animate_generated_model"})," para sair com esqueleto."]}),"\n",(0,n.jsx)(s.p,{children:"Dois fatos n\xe3o \xf3bvios que economizam dinheiro e rodada:"}),"\n",(0,n.jsxs)(s.ul,{children:["\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"O modelo base do Mint n\xe3o vem rigado."})," O esqueleto s\xf3 aparece no passo de anima\xe7\xe3o.\nO caminho barato para um personagem novo \xe9: gerar a base \u2192 rigar com ",(0,n.jsx)(s.strong,{children:"um"})," clipe \u2192\nusar o ",(0,n.jsx)(s.code,{children:"rigged_character_glb"})," dele \u2192 reaproveitar os clipes compartilhados."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"Os rigs Meshy compartilham os mesmos nomes de osso"})," (",(0,n.jsx)(s.code,{children:"Hips"}),", ",(0,n.jsx)(s.code,{children:"Spine"}),", ",(0,n.jsx)(s.code,{children:"Head"}),",\n",(0,n.jsx)(s.code,{children:"RightHand"}),"\u2026), ent\xe3o um pack de clipes gerado uma vez casa por nome em qualquer rig da\nfam\xedlia. \xc9 por isso que ",(0,n.jsx)(s.code,{children:"public/models/anims/"})," tem clipe compartilhado e clipe pr\xf3prio\nao mesmo tempo, e por isso existe o manifesto ",(0,n.jsx)(s.code,{children:"index.json"})," (",(0,n.jsx)(s.code,{children:"npm run anims"}),") \u2014 sem ele o\njogo pedia clipe de quem n\xe3o tem e enchia o console de 404."]}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"mint-assets.json"})," \xe9 o registro do que foi gerado: ",(0,n.jsx)(s.code,{children:"assetId"}),", ",(0,n.jsx)(s.code,{children:"chatUrl"})," e uma nota do que\ndeu errado na tentativa anterior. ",(0,n.jsx)(s.strong,{children:"Sem esse registro n\xe3o d\xe1 para revisar nem regerar"})," \u2014\no asset vira um bin\xe1rio sem proced\xeancia no meio do reposit\xf3rio."]}),"\n",(0,n.jsx)(s.h3,{id:"props-3d-tripo3d-e-meshy",children:"Props 3D: Tripo3D e Meshy"}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"tools/gen-asset.mjs"})," gera um prop por texto, baixa o GLB e grava ",(0,n.jsx)(s.strong,{children:"j\xe1 otimizado"})," em\n",(0,n.jsx)(s.code,{children:"public/models/props/"}),":"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:'node tools/gen-asset.mjs --prompt "caixa de som de baile" --id caixa_som\nnode tools/gen-asset.mjs --provider meshy --prompt "carro tunado" --id carro_tunado\nnode tools/gen-asset.mjs --resume --id caixa_som # tarefa j\xe1 paga\n'})}),"\n",(0,n.jsxs)(s.p,{children:["Tripo \xe9 o padr\xe3o; Meshy \xe9 a alternativa. ",(0,n.jsx)(s.code,{children:"--face-limit"})," (padr\xe3o 12000), ",(0,n.jsx)(s.code,{children:"--raw-only"})," para\npular a otimiza\xe7\xe3o e ",(0,n.jsx)(s.code,{children:"--timeout"})," completam as op\xe7\xf5es."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"Mapa n\xe3o precisa disso."})," Os mapas registrados s\xe3o geometria procedural em Three.js \u2014\nrua, barraco, beco, cal\xe7ada e rotunda s\xe3o caixa e plano, que \xe9 o que ",(0,n.jsx)(s.code,{children:"map_*.js"})," j\xe1 faz. O\nque vem de GLB s\xe3o ",(0,n.jsx)(s.strong,{children:"props"}),"."]}),"\n",(0,n.jsx)(s.h3,{id:"arte-2d-openrouter",children:"Arte 2D: OpenRouter"}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"tools/gen-image.mjs"})," \xe9 o irm\xe3o 2D: gera cartaz de fac\xe7\xe3o, wallpaper e splash por texto\n(+ imagens de refer\xeancia), e entrega o arquivo ",(0,n.jsx)(s.strong,{children:"j\xe1 enquadrado e comprimido"})," para a caixa\nem que a tela vai desenh\xe1-lo."]}),"\n",(0,n.jsxs)(s.p,{children:["O recorte mora no script de prop\xf3sito. Placa de fac\xe7\xe3o \xe9 uma caixa ",(0,n.jsx)(s.code,{children:"245\xd7620"})," com\n",(0,n.jsx)(s.code,{children:"background-size: cover"}),"; arte em paisagem entra nela mostrando ~26% da largura \u2014 foi\nassim que quatro cartazes de elenco viraram quatro retratos de UM personagem. O gerador\nn\xe3o oferece essa propor\xe7\xe3o, ent\xe3o quem publica \xe9 quem fecha a conta: gera no aspecto mais\npr\xf3ximo e recorta pelo centro at\xe9 a propor\xe7\xe3o ",(0,n.jsx)(s.strong,{children:"real"})," da caixa. Assim o que se olha antes\nde commitar \xe9 byte a byte o que o jogador v\xea."]}),"\n",(0,n.jsx)(s.h3,{id:"as-chaves",children:"As chaves"}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"TRIPO_API_KEY"}),", ",(0,n.jsx)(s.code,{children:"MESHY_API_KEY"})," e ",(0,n.jsx)(s.code,{children:"OPENROUTER_API_KEY"})," s\xe3o lidas de um ",(0,n.jsx)(s.code,{children:".env"})," na raiz \u2014\n",(0,n.jsx)(s.strong,{children:"gitignored, modo 600"}),". Tr\xeas regras que os dois scripts compartilham, cada uma com\nmotivo:"]}),"\n",(0,n.jsxs)(s.ol,{children:["\n",(0,n.jsxs)(s.li,{children:[(0,n.jsxs)(s.strong,{children:["A chave nunca vem de ",(0,n.jsx)(s.code,{children:"argv"}),"."]})," Argumento de linha de comando vaza no ",(0,n.jsx)(s.code,{children:"ps"})," de qualquer\nprocesso da m\xe1quina."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsxs)(s.strong,{children:["O header ",(0,n.jsx)(s.code,{children:"Authorization"})," s\xf3 sai para o host da pr\xf3pria API."]})," O GLB pronto vem de um\nCDN de terceiro (link assinado); mandar a chave junto no download entregaria credencial\npara um host que n\xe3o \xe9 o do provedor. H\xe1 allowlist, e ",(0,n.jsx)(s.code,{children:"redirect: 'error'"})," impede que um\n3xx carregue o header para outro dom\xednio."]}),"\n",(0,n.jsx)(s.li,{children:(0,n.jsxs)(s.strong,{children:["Nada \xe9 impresso sem passar por ",(0,n.jsx)(s.code,{children:"redact()"}),"."]})}),"\n"]}),"\n",(0,n.jsxs)(s.admonition,{type:"caution",children:[(0,n.jsxs)(s.mdxAdmonitionTitle,{children:["Essas tr\xeas chaves n\xe3o est\xe3o no ",(0,n.jsx)(s.code,{children:".env.example"})]}),(0,n.jsxs)(s.p,{children:["O ",(0,n.jsx)(s.code,{children:".env.example"})," cobre s\xf3 Supabase e o pacote de \xe1udio. As chaves de gera\xe7\xe3o de asset\nexistem apenas no ",(0,n.jsx)(s.code,{children:".env"})," do dono. Quem clonar e quiser gerar asset precisa cri\xe1-las \xe0 m\xe3o\ncom os nomes acima \u2014 est\xe1 documentado aqui e no cabe\xe7alho de cada script, n\xe3o no exemplo."]})]}),"\n",(0,n.jsx)(s.h2,{id:"otimiza\xe7\xe3o-de-glb-gltf-transform-e-meshoptimizer",children:"Otimiza\xe7\xe3o de GLB: gltf-transform e meshoptimizer"}),"\n",(0,n.jsxs)(s.p,{children:["Todo GLB que entra no reposit\xf3rio passa por ",(0,n.jsx)(s.code,{children:"@gltf-transform"})," (",(0,n.jsx)(s.code,{children:"dedup"}),", ",(0,n.jsx)(s.code,{children:"prune"}),",\n",(0,n.jsx)(s.code,{children:"textureCompress"})," com ",(0,n.jsx)(s.strong,{children:"sharp"})," para WebP) e, no caminho est\xe1tico, por ",(0,n.jsx)(s.strong,{children:"meshoptimizer"}),"."]}),"\n",(0,n.jsxs)(s.p,{children:["O motivo \xe9 um teto real: ",(0,n.jsx)(s.strong,{children:"250 MB na CrazyGames"}),". GLB cru de personagem chega em 4-5 MB,\ndominado por textura PNG 2K \u2014 e a otimiza\xe7\xe3o \xe9 quase toda de textura, n\xe3o de malha."]}),"\n",(0,n.jsxs)(s.p,{children:["Os scripts de pipeline vivem em ",(0,n.jsx)(s.code,{children:"tools/"}),": ",(0,n.jsx)(s.code,{children:"optimize-props.mjs"}),", ",(0,n.jsx)(s.code,{children:"optimize-static.mjs"}),",\n",(0,n.jsx)(s.code,{children:"optimize-fpvm.mjs"}),", ",(0,n.jsx)(s.code,{children:"optimize-tribos.mjs"}),", mais os de rig (",(0,n.jsx)(s.code,{children:"rig-from-donor.mjs"}),",\n",(0,n.jsx)(s.code,{children:"reskin-glb.mjs"}),", ",(0,n.jsx)(s.code,{children:"retarget-glb.mjs"}),") e os de inspe\xe7\xe3o (",(0,n.jsx)(s.code,{children:"inspect-glb.mjs"}),",\n",(0,n.jsx)(s.code,{children:"inspect-anim.mjs"}),", ",(0,n.jsx)(s.code,{children:"bones.mjs"}),")."]}),"\n",(0,n.jsx)(s.h2,{id:"playwright--todo-arn\xeas-que-precisa-de-browser",children:"Playwright \u2014 todo arn\xeas que precisa de browser"}),"\n",(0,n.jsxs)(s.p,{children:["R\xe9gua que depende de ",(0,n.jsx)(s.strong,{children:"pixel"})," roda em Chromium via Playwright. \xc9 o caso de\n",(0,n.jsx)(s.code,{children:"tools/eval/*-capture.mjs"}),", ",(0,n.jsx)(s.code,{children:"telas-*.mjs"}),", ",(0,n.jsx)(s.code,{children:"select-inflate.mjs"}),", ",(0,n.jsx)(s.code,{children:"crash-watch.mjs"})," e\n",(0,n.jsx)(s.code,{children:"fv-verify.mjs"}),", entre outros."]}),"\n",(0,n.jsx)(s.p,{children:"Duas coisas que voc\xea precisa saber antes de rodar qualquer um:"}),"\n",(0,n.jsxs)(s.ul,{children:["\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"Custa caro."})," Render por software (SwiftShader) roda o jogo a ~0,3 FPS; uma captura\nin-game custa minutos por mapa/aspecto. Foi exatamente esse custo que empurrou o quality gate\npara node puro \u2014 e \xe9 por isso que as invariantes de pixel (",(0,n.jsx)(s.code,{children:"PX1"}),"\u2013",(0,n.jsx)(s.code,{children:"PX4"}),") est\xe3o\n",(0,n.jsx)(s.strong,{children:"puladas"}),", com o motivo dito."]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.strong,{children:"Uma sess\xe3o por vez."}),' Duas capturas headless em paralelo derrubam o boot e produzem\n"countdown travado" que parece bug e \xe9 carga. Um \xfanico agente roda browser.']}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:["Alguns arn\xeases precisam do servidor no ar: ",(0,n.jsx)(s.code,{children:"npm run eval:serve &"})," antes."]}),"\n",(0,n.jsx)(s.h2,{id:"skills-de-agente",children:"Skills de agente"}),"\n",(0,n.jsxs)(s.p,{children:["Este reposit\xf3rio versiona ",(0,n.jsx)(s.strong,{children:"skills"})," \u2014 instru\xe7\xf5es empacotadas que um agente carrega antes\nde trabalhar. Elas vivem em ",(0,n.jsx)(s.code,{children:".agents/skills/"}),", e ",(0,n.jsx)(s.code,{children:".claude/skills/"})," s\xe3o symlinks para l\xe1."]}),"\n","\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Contagem"}),(0,n.jsx)(s.th,{style:{textAlign:"right"},children:"Quanto"}),(0,n.jsx)(s.th,{children:"O que significa"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:["Declaradas no ",(0,n.jsx)(s.code,{children:"skills-lock.json"})]}),(0,n.jsx)(s.td,{style:{textAlign:"right"},children:"39"}),(0,n.jsxs)(s.td,{children:["com ",(0,n.jsx)(s.code,{children:"source"}),", ",(0,n.jsx)(s.code,{children:"skillPath"})," e ",(0,n.jsx)(s.code,{children:"computedHash"})," \u2014 skill de terceiro que mudar de conte\xfado \xe9 detect\xe1vel"]})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Versionadas (chegam em quem clona)"}),(0,n.jsx)(s.td,{style:{textAlign:"right"},children:"10"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"git ls-files .agents/skills"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:["\u2026dessas, com ",(0,n.jsx)(s.code,{children:"SKILL.md"})," no git"]}),(0,n.jsx)(s.td,{style:{textAlign:"right"},children:"10"}),(0,n.jsx)(s.td,{children:"\xe9 o que um clone limpo consegue ler"})]})]})]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"As contagens divergem de prop\xf3sito, e a diferen\xe7a \xe9 o fato:"})," a maioria das skills \xe9 de terceiro, fixada por hash no lock e baixada sob demanda. Quem clonar o reposit\xf3rio recebe o lock inteiro e s\xf3 uma parte do conte\xfado. Publicar s\xf3 uma das contagens esconderia exatamente o que o contribuidor precisa saber."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:".claude/skills/"})," s\xe3o ",(0,n.jsx)(s.strong,{children:"symlinks"})," para ",(0,n.jsx)(s.code,{children:".agents/skills/"})," \u2014 uma c\xf3pia s\xf3, dois nomes, porque o Claude Code l\xea de ",(0,n.jsx)(s.code,{children:".claude/"})," e outros arn\xeases leem de ",(0,n.jsx)(s.code,{children:".agents/"}),"."]}),"\n",(0,n.jsxs)(s.p,{children:["A skill do loop desta casa, ",(0,n.jsx)(s.strong,{children:(0,n.jsx)(s.code,{children:"gauntlet-fps"})}),", \xe9 a \xfanica que nasceu aqui: vive em ",(0,n.jsx)(s.code,{children:".claude/skills/gauntlet-fps/SKILL.md"}),", n\xe3o \xe9 symlink e n\xe3o entra no lock."]}),"\n",(0,n.jsxs)(s.blockquote,{children:["\n",(0,n.jsxs)(s.p,{children:["Bloco gerado por ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,n.jsx)(s.code,{children:"git ls-files .agents/skills \xb7 skills-lock.json"})]}),"\n"]}),"\n","\n",(0,n.jsx)(s.p,{children:"A grande maioria \xe9 de terceiro e cobre Three.js (materiais, ilumina\xe7\xe3o, shaders,\np\xf3s-processamento, carregamento de glTF, geometria, anima\xe7\xe3o) e game design. Elas s\xe3o\ncontexto opcional: nada no jogo depende delas."}),"\n",(0,n.jsx)(s.h3,{id:"o-gauntlet-loop",children:"O gauntlet loop"}),"\n",(0,n.jsxs)(s.p,{children:["A skill que ",(0,n.jsx)(s.strong,{children:"n\xe3o"})," \xe9 de terceiro \xe9 a ",(0,n.jsx)(s.code,{children:"gauntlet-fps"}),", e ela codifica o ciclo de trabalho\ndesta casa:"]}),"\n",(0,n.jsxs)(s.blockquote,{children:["\n",(0,n.jsx)(s.p,{children:(0,n.jsx)(s.strong,{children:"cr\xedtico adversarial \u2192 construtores em paralelo \u2192 captura medida \u2192 verifica\xe7\xe3o A/B \u2192\nca\xe7ador de regress\xf5es"})}),"\n"]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"Quando usar:"}),' melhorar, avaliar ou revisar qualquer parte do jogo \u2014 gr\xe1ficos,\nfidelidade de mapa, feel de arma, menu, HUD, bots, movimento \u2014 e quando algo \xe9 reportado\ncomo feio, estranho ou "n\xe3o parece profissional". ',(0,n.jsx)(s.strong,{children:"Quando n\xe3o usar:"})," tarefa mec\xe2nica de\numa linha, ou pergunta conceitual que n\xe3o mexe no jogo."]}),"\n",(0,n.jsxs)(s.p,{children:["O ciclo inteiro \u2014 as tr\xeas regras, o problema que cada uma resolve e o caso medido de cada\numa \u2014 tem p\xe1gina pr\xf3pria: ",(0,n.jsx)(s.strong,{children:(0,n.jsx)(s.a,{href:"/docs/instrumentacao-ai",children:"Instrumenta\xe7\xe3o de IA"})}),". Esta se\xe7\xe3o\nexiste s\xf3 para dizer que a skill existe e quando acion\xe1-la."]}),"\n",(0,n.jsx)(s.h2,{id:"a-documenta\xe7\xe3o",children:"A documenta\xe7\xe3o"}),"\n",(0,n.jsxs)(s.p,{children:["Esta doc \xe9 um ",(0,n.jsx)(s.strong,{children:"Docusaurus separado"}),", em ",(0,n.jsx)(s.code,{children:"docs/"}),", com o pr\xf3prio ",(0,n.jsx)(s.code,{children:"package.json"})," e o\npr\xf3prio ",(0,n.jsx)(s.code,{children:"node_modules"}),". Nada aqui \xe9 importado pelo jogo nem pelo site."]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:"cd docs && npm install && npm start # http://localhost:3000/docs/\ncd docs && npm run build # docs/build/\ncd docs && npm run build:site # buildar PARA DENTRO de public/docs/\n"})}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"baseUrl"})," \xe9 ",(0,n.jsx)(s.code,{children:"/docs/"})," porque a sa\xedda pode ser buildada para ",(0,n.jsx)(s.code,{children:"public/docs/"}),", e o Astro copia\n",(0,n.jsx)(s.code,{children:"public/"})," inteiro para ",(0,n.jsx)(s.code,{children:"dist/client/"}),"."]}),"\n",(0,n.jsx)(s.admonition,{title:"Todo n\xfamero desta p\xe1gina \xe9 gerado",type:"tip",children:(0,n.jsxs)(s.p,{children:["As tabelas acima saem de ",(0,n.jsx)(s.code,{children:"node tools/gen-docs.mjs"})," e s\xe3o conferidas por\n",(0,n.jsx)(s.code,{children:"npm run docs:check"}),", dentro do ",(0,n.jsx)(s.code,{children:"check:fast"}),". O mecanismo \u2014 o que entra num bloco gerado, o\nque fica escrito \xe0 m\xe3o, e como colar um bloco novo \u2014 est\xe1 em\n",(0,n.jsx)(s.a,{href:"/docs/arquitetura#o-que-%C3%A9-gerado-e-o-que-n%C3%A3o-%C3%A9",children:"Arquitetura"}),"."]})})]})}function h(e={}){const{wrapper:s}={...(0,d.R)(),...e.components};return s?(0,n.jsx)(s,{...e,children:(0,n.jsx)(t,{...e})}):t(e)}},8453(e,s,o){o.d(s,{R:()=>a,x:()=>i});var r=o(6540);const n={},d=r.createContext(n);function a(e){const s=r.useContext(d);return r.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function i(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),r.createElement(d.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/assets/js/e8699d1d.1e1639d2.js b/public/docs/assets/js/e8699d1d.ac593cb4.js similarity index 99% rename from public/docs/assets/js/e8699d1d.1e1639d2.js rename to public/docs/assets/js/e8699d1d.ac593cb4.js index 3070af9f5..de1b66e26 100644 --- a/public/docs/assets/js/e8699d1d.1e1639d2.js +++ b/public/docs/assets/js/e8699d1d.ac593cb4.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[475],{1669(e,o,a){a.r(o),a.d(o,{assets:()=>c,contentTitle:()=>d,default:()=>m,frontMatter:()=>i,metadata:()=>r,toc:()=>t});const r=JSON.parse('{"id":"quality-gates","title":"O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o","description":"O que \xe9 uma invariante neste repo, como se escreve uma, as duas leis da casa com o caso real de cada uma, e o teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua.","source":"@site/docs/quality-gates.md","sourceDirName":".","slug":"/quality-gates","permalink":"/docs/quality-gates","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/quality-gates.md","tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"id":"quality-gates","title":"O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o","sidebar_label":"Quality gates","sidebar_position":4,"description":"O que \xe9 uma invariante neste repo, como se escreve uma, as duas leis da casa com o caso real de cada uma, e o teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua."},"sidebar":"dev","previous":{"title":"Instrumenta\xe7\xe3o de IA","permalink":"/docs/instrumentacao-ai"},"next":{"title":"BotBrain","permalink":"/docs/botbrain"}}');var s=a(4848),n=a(8453);const i={id:"quality-gates",title:"O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o",sidebar_label:"Quality gates",sidebar_position:4,description:"O que \xe9 uma invariante neste repo, como se escreve uma, as duas leis da casa com o caso real de cada uma, e o teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua."},d="O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o",c={},t=[{value:"Por que ele existe",id:"por-que-ele-existe",level:2},{value:"O que \xe9 uma invariante aqui",id:"o-que-\xe9-uma-invariante-aqui",level:2},{value:"Severidade",id:"severidade",level:3},{value:"As duas leis da casa",id:"as-duas-leis-da-casa",level:2},{value:"Lei 1 \u2014 Inten\xe7\xe3o que n\xe3o vira invariante \xe9 otimizada para fora",id:"lei-1--inten\xe7\xe3o-que-n\xe3o-vira-invariante-\xe9-otimizada-para-fora",level:3},{value:"Lei 2 \u2014 Teto sem proced\xeancia \xe9 opini\xe3o",id:"lei-2--teto-sem-proced\xeancia-\xe9-opini\xe3o",level:3},{value:"Teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua",id:"teste-de-muta\xe7\xe3o-da-pr\xf3pria-r\xe9gua",level:2},{value:"O caso: 20/22 verde com a corre\xe7\xe3o removida",id:"o-caso-2022-verde-com-a-corre\xe7\xe3o-removida",level:3},{value:"N\xe3o foi um caso isolado \u2014 foram tr\xeas",id:"n\xe3o-foi-um-caso-isolado--foram-tr\xeas",level:3},{value:"Muta\xe7\xe3o como coisa de primeira classe: ui-check.mjs",id:"muta\xe7\xe3o-como-coisa-de-primeira-classe-ui-checkmjs",level:3},{value:"Como escrever uma invariante",id:"como-escrever-uma-invariante",level:2},{value:"Anti-padr\xf5es que j\xe1 custaram caro aqui",id:"anti-padr\xf5es-que-j\xe1-custaram-caro-aqui",level:3},{value:"Esta p\xe1gina \xe9 a doutrina. O passo a passo \xe9 uma skill",id:"esta-p\xe1gina-\xe9-a-doutrina-o-passo-a-passo-\xe9-uma-skill",level:2},{value:"Port\xf5es que N\xc3O cabem no check, e por qu\xea",id:"port\xf5es-que-n\xe3o-cabem-no-check-e-por-qu\xea",level:2},{value:"Rodar o quality gate",id:"rodar-o-quality-gate",level:2}];function l(e){const o={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,n.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(o.header,{children:(0,s.jsx)(o.h1,{id:"o-quality-gate-invariantes-proced\xeancia-e-muta\xe7\xe3o",children:"O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o"})}),"\n",(0,s.jsxs)(o.p,{children:["O quality gate deste reposit\xf3rio \xe9 um arquivo: ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs"}),". Ele roda em node puro\ne sai com c\xf3digo 1 se qualquer invariante ",(0,s.jsx)(o.strong,{children:"cr\xedtica"})," falhar. \xc9 o que o CI executa em todo\nPR (",(0,s.jsx)(o.code,{children:".github/workflows/ci.yml"}),")."]}),"\n","\n",(0,s.jsxs)(o.ul,{children:["\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs"}),": ",(0,s.jsx)(o.strong,{children:"2.275 linhas"}),", ",(0,s.jsx)(o.strong,{children:"65 identificadores de invariante declarados"})," (",(0,s.jsx)(o.code,{children:"put()"}),"), dos quais ",(0,s.jsx)(o.strong,{children:"28"})," t\xeam caminho de ",(0,s.jsx)(o.code,{children:"skip()"})," declarado."]}),"\n",(0,s.jsxs)(o.li,{children:["O arn\xeas inteiro s\xe3o ",(0,s.jsx)(o.strong,{children:"192 scripts"})," em ",(0,s.jsx)(o.code,{children:"tools/eval/"})," (",(0,s.jsx)(o.code,{children:".mjs"})," + ",(0,s.jsx)(o.code,{children:".py"}),"), mais ",(0,s.jsx)(o.strong,{children:"54 scripts"})," de pipeline em ",(0,s.jsx)(o.code,{children:"tools/"}),"."]}),"\n",(0,s.jsxs)(o.li,{children:["Quantas invariantes rodam como ",(0,s.jsx)(o.strong,{children:"cr\xedticas"})," numa execu\xe7\xe3o ",(0,s.jsx)(o.strong,{children:"n\xe3o \xe9 deriv\xe1vel do fonte"}),": depende de qual insumo existe na m\xe1quina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse n\xfamero s\xf3 sai rodando o quality gate \u2014 e o lugar dele \xe9 o cabe\xe7alho do ",(0,s.jsx)(o.code,{children:"KNOWN-BUGS.md"}),", atualizado com sa\xedda real."]}),"\n"]}),"\n",(0,s.jsx)(o.p,{children:"Reproduza:"}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-bash",children:"grep -o \"put('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l\ngrep -o \"skip('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l\n"})}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["Bloco gerado por ",(0,s.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,s.jsx)(o.code,{children:"grep -o \"put('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l"})]}),"\n"]}),"\n","\n",(0,s.jsxs)(o.p,{children:["O terceiro item acima \xe9 a distin\xe7\xe3o que mais confunde quem chega: ",(0,s.jsx)(o.strong,{children:"identificador\ndeclarado \u2260 invariante avaliada."})," V\xe1rias viram ",(0,s.jsx)(o.code,{children:"skip"})," em vez de ",(0,s.jsx)(o.code,{children:"put"})," quando falta o\ninsumo delas (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). ",(0,s.jsx)(o.code,{children:"skip"})," \xe9\n",(0,s.jsx)(o.strong,{children:"quality gate verde por aus\xeancia de dado"}),', e \xe9 por isso que ele sempre carrega o motivo. Ver\n"Severidade", abaixo.']}),"\n",(0,s.jsx)(o.p,{children:"Esta p\xe1gina \xe9 a mais \xfatil do site. Se voc\xea s\xf3 for ler uma, leia esta."}),"\n",(0,s.jsx)(o.h2,{id:"por-que-ele-existe",children:"Por que ele existe"}),"\n",(0,s.jsxs)(o.p,{children:["Do cabe\xe7alho do pr\xf3prio arquivo, ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:5-19"}),":"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["O dono passou 3 dias num ciclo em que cada rodada consertava uma coisa e quebrava\noutra, e a gente s\xf3 descobria uma rodada depois. A causa n\xe3o era falta de cuidado:\nera falta de R\xc9GUA. Um cr\xedtico (humano ou agente) julga screenshot; consist\xeancia e\nflow s\xe3o propriedades do jogo ",(0,s.jsx)(o.strong,{children:"EM MOVIMENTO"}),", e quase todo defeito que ele reportou\nn\xe3o \xe9 gosto \u2014 \xe9 invariante violada."]}),"\n"]}),"\n",(0,s.jsx)(o.p,{children:"E a tradu\xe7\xe3o, que \xe9 a coisa mais importante deste reposit\xf3rio inteiro:"}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"O que o dono disse"}),(0,s.jsx)(o.th,{children:"Qual invariante isso virou"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"as m\xe3os est\xe3o soltas no ar"'}),(0,s.jsx)(o.td,{children:"dist\xe2ncia m\xe3o\u2194grip tem um teto"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"a arma aponta pra baixo"'}),(0,s.jsx)(o.td,{children:"o cano tem um \xe2ngulo m\xe1ximo"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"no ADS n\xe3o vejo a arma nem a mira"'}),(0,s.jsx)(o.td,{children:"a arma tem \xe1rea m\xednima e m\xe1xima"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"sniper sem zoom"'}),(0,s.jsx)(o.td,{children:"FOV mirando < FOV de quadril"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"v\xe1rias armas com visual igual"'}),(0,s.jsx)(o.td,{children:"silhuetas t\xeam que diferir"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"o bot atira do nada"'}),(0,s.jsx)(o.td,{children:"dano exige LOS anterior"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"tem 2 me eliminando"'}),(0,s.jsx)(o.td,{children:"1 killfeed por morte"})]})]})]}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:20-21"}),":"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsx)(o.p,{children:(0,s.jsx)(o.strong,{children:"REGRA DE OURO: nada \xe9 commitado com invariante VERMELHA. E todo bug novo que o dono\nreportar vira uma invariante aqui \u2014 \xe9 assim que ele nunca volta."})}),"\n"]}),"\n",(0,s.jsx)(o.h2,{id:"o-que-\xe9-uma-invariante-aqui",children:"O que \xe9 uma invariante aqui"}),"\n",(0,s.jsxs)(o.p,{children:["Uma invariante \xe9 uma ",(0,s.jsx)(o.strong,{children:"propriedade do jogo que d\xe1 pra medir sem um humano olhando"}),", com\num teto ou uma faixa que tem proced\xeancia. N\xe3o \xe9 teste unit\xe1rio: quase nenhuma invariante\ntesta uma fun\xe7\xe3o. Elas medem o ",(0,s.jsx)(o.strong,{children:"estado do jogo rodando de verdade"}),"."]}),"\n",(0,s.jsx)(o.p,{children:"Tr\xeas formas, todas presentes no arquivo:"}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"1. Lida do c\xf3digo-fonte."})," Barata, roda em milissegundos, pega classes inteiras de bug.\nExemplo real, ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:1439-1446"}),":"]}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-js",children:"// ARM1 \u2014 toda arma com luneta precisa de zoom de verdade. \"Snipers sem zoom\"\n// \xe9 reclama\xe7\xe3o literal; a solu\xe7\xe3o N\xc3O \xe9 tirar a luneta, \xe9 fazer a certa.\nconst bloco = gsrc.slice(0, gsrc.indexOf('};', gsrc.indexOf('const WEAPONS')) + 2);\nconst linhas = bloco.split('\\n').filter((l) => /^\\s*\\w+:\\s*\\{/.test(l));\nconst semZoom = linhas.filter((l) => /scope:\\s*true/.test(l) && !/spreadScope/.test(l))\n .map((l) => l.trim().split(':')[0]);\nput('ARM1', 'toda arma com scope:true declara spreadScope', semZoom.length === 0,\n semZoom.length ? semZoom.join(', ') : `${linhas.length} armas conferidas`);\n"})}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"2. Medida no jogo real rodando em node."})," ",(0,s.jsx)(o.code,{children:"tools/eval/harness.mjs"})," sobe a classe ",(0,s.jsx)(o.code,{children:"Game"}),"\nde verdade, com os mapas de verdade, com DOM/canvas stubado. \xc9 o ",(0,s.jsx)(o.strong,{children:"c\xf3digo de produ\xe7\xe3o"}),"\nque \xe9 medido, n\xe3o uma reimplementa\xe7\xe3o \u2014 ",(0,s.jsx)(o.code,{children:"tools/eval/botsim.mjs:8-9"}),": ",(0,s.jsx)(o.em,{children:'"se o n\xfamero\nmelhorar aqui, melhorou no jogo"'}),". Daqui saem BOT1\u2013BOT8, MAP1\u2013MAP3, CTF1, MAT1/MAT2,\nFOG1, TEX1, VM14, MOD1/MOD2."]}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"3. Medida na geometria dos assets."})," ",(0,s.jsx)(o.code,{children:"vm-mint-audit.mjs"})," abre todos os GLBs de arma com um\nparser de GLB pr\xf3prio e projeta o viewmodel na tela. Daqui saem VM1\u2013VM19."]}),"\n",(0,s.jsxs)(o.p,{children:["O que ",(0,s.jsx)(o.strong,{children:"n\xe3o"})," cabe aqui: invariante que exige pixel de browser. Essas est\xe3o marcadas\n",(0,s.jsx)(o.code,{children:"browser"})," e s\xe3o puladas, com o motivo dito \u2014 SwiftShader custa ~4 min por carga de mapa\nnesta m\xe1quina (",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:99"}),")."]}),"\n",(0,s.jsx)(o.h3,{id:"severidade",children:"Severidade"}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"put(id, desc, ok, evid, sev)"})," aceita ",(0,s.jsx)(o.code,{children:"'crit'"})," (padr\xe3o) ou ",(0,s.jsx)(o.code,{children:"'warn'"}),"\n(",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:81-82"}),"). Cr\xedtica vermelha reprova o PR. Warn \xe9 ru\xeddo medido que\nalgu\xe9m precisa olhar mas n\xe3o bloqueia \u2014 \xe9 onde vivem BOT1/BOT2/BOT3/BOT6/BOT7, ARM4 e\nARM5. ",(0,s.jsx)(o.code,{children:"skip()"})," \xe9 o terceiro estado, e ele \xe9 ",(0,s.jsx)(o.strong,{children:"perigoso"}),": quality gate verde por aus\xeancia de\ndado. Por isso todo ",(0,s.jsx)(o.code,{children:"skip"})," carrega o motivo."]}),"\n",(0,s.jsx)(o.h2,{id:"as-duas-leis-da-casa",children:"As duas leis da casa"}),"\n",(0,s.jsx)(o.h3,{id:"lei-1--inten\xe7\xe3o-que-n\xe3o-vira-invariante-\xe9-otimizada-para-fora",children:"Lei 1 \u2014 Inten\xe7\xe3o que n\xe3o vira invariante \xe9 otimizada para fora"}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsxs)(o.strong,{children:["Fonte: ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:452-461"}),"."]})," O caso, literal:"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["a rodada anterior levou o quality gate de ",(0,s.jsx)(o.strong,{children:"16/21 para 19/21 sem afrouxar um teto sequer"})," e\nmesmo assim foi ",(0,s.jsx)(o.strong,{children:"REPROVADA"})," pelo dono, porque para fechar VM5/VM10 ela ",(0,s.jsxs)(o.strong,{children:["ZEROU o\n",(0,s.jsx)(o.code,{children:"VM_OFF"})," y"]}),' e mudou o look em sil\xeancio. Nenhuma invariante codificava "onde fica a\nboca do cano", ent\xe3o a m\xe9trica foi otimizada e a INTEN\xc7\xc3O foi destru\xedda. Lei de\nGoodhart, na \xedntegra. ',(0,s.jsx)(o.strong,{children:"INTEN\xc7\xc3O QUE N\xc3O VIRA INVARIANTE \xc9 OTIMIZADA PARA FORA."})]}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["Leia de novo o que aconteceu, porque \xe9 contraintuitivo: o agente ",(0,s.jsx)(o.strong,{children:"n\xe3o trapaceou"}),". Ele\nn\xe3o afrouxou nenhum teto. Ele subiu o placar de verdade. E o resultado foi pior, porque\n",(0,s.jsx)(o.code,{children:"VM_OFF[1]"})," \xe9 o termo que ",(0,s.jsx)(o.strong,{children:"domina a posi\xe7\xe3o da arma na tela"})," \u2014 ",(0,s.jsx)(o.code,{children:"public/js/game.js:555"}),"\ndeclara ",(0,s.jsx)(o.code,{children:"VM_OFF = [0.03, -0.1000, 0]"}),", e ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:1163"})," mede a\nsensibilidade: ",(0,s.jsx)(o.em,{children:'"tirar o recuoZ move o grip 3,5 cm; tirar o VM_OFF move 23 cm"'}),"."]}),"\n",(0,s.jsx)(o.p,{children:"Zerar esse termo fechou duas invariantes e apagou a decis\xe3o est\xe9tica que o dono tinha\ntomado \u2014 que n\xe3o estava escrita em lugar nenhum que a r\xe9gua pudesse ler."}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"A corre\xe7\xe3o n\xe3o foi punir o agente. Foi escrever a inten\xe7\xe3o como invariante."})," Hoje\nexiste a VM12 (",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:497"}),"): ",(0,s.jsx)(o.em,{children:'"look CS 1.6: boca do cano LOGO abaixo\nda mira (y entre 0,50 e 0,62) nos 2 aspectos"'}),". Com ela no lugar, a mesma otimiza\xe7\xe3o\nfica ",(0,s.jsx)(o.strong,{children:"vermelha"}),"."]}),"\n",(0,s.jsx)(o.p,{children:"E a consequ\xeancia operacional, do mesmo coment\xe1rio:"}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["Quem quiser mudar o look tem que mudar ",(0,s.jsx)(o.strong,{children:"ESTE teto"})," explicitamente, num diff que o\ndono v\xea, em vez de mexer no ",(0,s.jsx)(o.code,{children:"VM_OFF"}),' e reportar "+3 invariantes".']}),"\n"]}),"\n",(0,s.jsx)(o.admonition,{title:"O que isso significa pro seu PR",type:"tip",children:(0,s.jsxs)(o.p,{children:["Se a sua mudan\xe7a melhora o placar do quality gate, a primeira pergunta \xe9: ",(0,s.jsx)(o.strong,{children:"o que eu mudei que\no quality gate n\xe3o olha?"}),' Se a resposta for "o look", "o feel" ou "a sensa\xe7\xe3o", escreva a\ninvariante antes de mandar o PR \u2014 ou explique no PR por que ela n\xe3o cabe.']})}),"\n",(0,s.jsx)(o.h3,{id:"lei-2--teto-sem-proced\xeancia-\xe9-opini\xe3o",children:"Lei 2 \u2014 Teto sem proced\xeancia \xe9 opini\xe3o"}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsxs)(o.strong,{children:["Fonte: ",(0,s.jsx)(o.code,{children:"tools/eval/ref-measure.py:1-40"}),"."]})," Essa docstring \xe9 a doutrina da casa. O caso:"]}),"\n",(0,s.jsxs)(o.p,{children:["Durante ",(0,s.jsx)(o.strong,{children:"tr\xeas dias"})," o quality gate de armas foi resolvido contra n\xfameros ",(0,s.jsx)(o.strong,{children:"asseridos"}),":"]}),"\n",(0,s.jsxs)(o.ul,{children:["\n",(0,s.jsxs)(o.li,{children:["A VM12 exigia ",(0,s.jsx)(o.em,{children:'"boca do cano em y \u2265 0,66"'}),"."]}),"\n",(0,s.jsxs)(o.li,{children:["O doc do ",(0,s.jsx)(o.code,{children:"vmattach.js"})," dizia ",(0,s.jsx)(o.em,{children:'"coronha INTEIRA no canto"'}),"."]}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["Nenhum dos dois foi medido em imagem nenhuma. Segundo ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:461-463"}),",\no piso 0,66 veio de um coment\xe1rio do ",(0,s.jsx)(o.code,{children:"public/js/vmattach.js"})," \u2014 ",(0,s.jsx)(o.em,{children:'"a boca fica a ~0,66H"'})," \u2014\nque por sua vez veio de um v\xeddeo assistido. (O coment\xe1rio do quality gate aponta para\n",(0,s.jsx)(o.code,{children:"vmattach.js:387-392"}),"; hoje o texto est\xe1 em ",(0,s.jsx)(o.code,{children:"vmattach.js:395"}),", porque o arquivo andou. \xc9\nexatamente o motivo de o ",(0,s.jsx)(o.code,{children:"ARCH.md"})," ser gerado \u2014 ver ",(0,s.jsx)(o.a,{href:"/docs/arquitetura",children:"Arquitetura"}),".)"]}),"\n",(0,s.jsxs)(o.p,{children:["O dono olhou o resultado e disse, literal (",(0,s.jsx)(o.code,{children:"ref-measure.py:14-17"}),"):"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsx)(o.p,{children:'"est\xe1 diferente do CS 1.6 e do Quake e do UT; nesses 3 a arma est\xe1 sempre no canto\ninferior direito e a coronha sempre FORA; depois de 3 dias e uma pasta inteira de\nrefer\xeancia nem voc\xea nem o Kimi entendeu isso."'}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["A\xed os frames foram ",(0,s.jsx)(o.strong,{children:"medidos"}),". ",(0,s.jsx)(o.code,{children:"tools/eval/ref-measure.py"})," faz segmenta\xe7\xe3o por cor no\nquadrante inferior-direito, pega a maior componente conexa, e escreve\n",(0,s.jsx)(o.code,{children:"tools/eval/ref_viewmodel.json"}),". Resultado:"]}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"Frame"}),(0,s.jsx)(o.th,{children:"Boca (x, y)"}),(0,s.jsx)(o.th,{style:{textAlign:"right"},children:"\xc1rea na tela"}),(0,s.jsx)(o.th,{style:{textAlign:"right"},children:"\xc2ngulo do eixo"}),(0,s.jsx)(o.th,{children:"Cruza a borda direita?"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"cs16_ak_dust.jpg"})}),(0,s.jsxs)(o.td,{children:["0,564 ; ",(0,s.jsx)(o.strong,{children:"0,513"})]}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"9,76%"}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"28,0\xb0"}),(0,s.jsx)(o.td,{children:"sim"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"cs16_m4_dust.jpg"})}),(0,s.jsxs)(o.td,{children:["0,569 ; ",(0,s.jsx)(o.strong,{children:"0,598"})]}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"9,78%"}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"34,8\xb0"}),(0,s.jsx)(o.td,{children:"sim"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"valorant_vandal.jpg"})}),(0,s.jsxs)(o.td,{children:["0,648 ; ",(0,s.jsx)(o.strong,{children:"0,587"})]}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"13,09%"}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"4,6\xb0"}),(0,s.jsx)(o.td,{children:"sim"})]})]})]}),"\n",(0,s.jsx)(o.p,{children:(0,s.jsx)(o.strong,{children:"Os dois n\xfameros asseridos estavam errados:"})}),"\n",(0,s.jsxs)(o.ol,{children:["\n",(0,s.jsxs)(o.li,{children:["A boca do CS 1.6 fica em ",(0,s.jsx)(o.strong,{children:"0,513\u20130,598"})," \u2014 logo abaixo da mira (0,5), 1 a 10 pontos\npercentuais abaixo do centro. N\xe3o em 0,66\u20130,93. O piso errado estava mantendo a nossa\narma ",(0,s.jsx)(o.strong,{children:"afundada"})," em 0,667\u20130,816 (",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:472-475"}),")."]}),"\n",(0,s.jsxs)(o.li,{children:["A coronha ",(0,s.jsx)(o.strong,{children:"SAI pela quina"})," nos 3 frames. Sair \xe9 o padr\xe3o, n\xe3o o defeito\n(",(0,s.jsx)(o.code,{children:"ref_viewmodel.json"})," \u2192 ",(0,s.jsx)(o.code,{children:"faixas.cruzaBordaDireita: true"})," nos 3)."]}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["E o dano colateral: com o teto falso, o solver da rodada anterior ",(0,s.jsx)(o.em,{children:'"provou"'})," que 3% de\n\xe1rea era invi\xe1vel. A prova estava certa ",(0,s.jsx)(o.strong,{children:"contra aquele teto"})," \u2014 e o teto \xe9 que era falso\n(",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:476-478"}),")."]}),"\n",(0,s.jsxs)(o.p,{children:["A regra que ficou, ",(0,s.jsx)(o.code,{children:"tools/eval/ref-measure.py:21-22"}),":"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsx)(o.p,{children:(0,s.jsx)(o.strong,{children:"TETO DE INVARIANTE S\xd3 ENTRA COM PROCED\xcaNCIA \u2014 arquivo de refer\xeancia, pixel medido, e\neste script reproduzindo o n\xfamero. N\xfamero sem imagem \xe9 opini\xe3o."})}),"\n"]}),"\n",(0,s.jsx)(o.p,{children:"Hoje as invariantes de enquadramento carregam a proced\xeancia no pr\xf3prio texto: VM1 (faixa\n0,50\u20130,60, ref 0,520\u20130,565), VM3 (22\u201342\xb0, ref 28,0\xb0 e 34,8\xb0), VM5 (6\u201316%, ref\n9,76\u201313,09%), VM12 (0,50\u20130,62, ref 0,513\u20130,598), VM16 (fatia na borda direita 0,02\u20130,20,\nref 0,053\u20130,095)."}),"\n",(0,s.jsx)(o.admonition,{title:"Proced\xeancia inclui admitir o que a imagem N\xc3O mede",type:"note",children:(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:599-602"}),' recusa criar um teto para "quanto da arma fica fora\ndo quadro", porque o que est\xe1 fora \xe9 invis\xedvel na foto \u2014 n\xe3o d\xe1 pra saber se a coronha do\nAK termina 5 cm ou 50 cm al\xe9m da borda. Os n\xfameros continuam no JSON como ',(0,s.jsx)(o.strong,{children:"evid\xeancia,\nsem gate"}),". Isso \xe9 proced\xeancia levada a s\xe9rio: a r\xe9gua diz onde ela para de saber."]})}),"\n",(0,s.jsxs)(o.p,{children:["E o mesmo rigor morde quem escreveu a r\xe9gua, no caso mais desconfort\xe1vel poss\xedvel: ",(0,s.jsx)(o.strong,{children:"as\nfotos de refer\xeancia de personagem chegaram, foram medidas, e foram REPROVADAS pela pr\xf3pria\nr\xe9gua."})," ",(0,s.jsx)(o.code,{children:"tools/eval/char-probe.mjs:25-45"})," conta o epis\xf3dio inteiro \u2014 ",(0,s.jsx)(o.code,{children:"references/funkeiros/"}),"\ntem 23 arquivos e ",(0,s.jsx)(o.code,{children:"references/palhacos/"})," tem 21, todos passados pelo ",(0,s.jsx)(o.code,{children:"ref-body.py"}),", com as\nm\xe1scaras ",(0,s.jsx)(o.strong,{children:"olhadas"})," (",(0,s.jsx)(o.code,{children:"--masks"}),"). O veredito, dito na cara pelo pr\xf3prio coment\xe1rio: s\xe3o\nselfies e closes; a segmenta\xe7\xe3o heur\xedstica devolve a m\xe3o, um peda\xe7o de jaqueta ou o cabelo\nde outra pessoa no fundo, e a raz\xe3o ombro/altura sai entre ",(0,s.jsx)(o.strong,{children:"0,42 e 3,78"})," quando um humano\nmede 0,259. Sobra ~1 foto de corpo inteiro utiliz\xe1vel \u2014 n\xe3o \xe9 amostra."]}),"\n",(0,s.jsxs)(o.p,{children:["O ",(0,s.jsx)(o.code,{children:"ref-body.py"})," exige ",(0,s.jsx)(o.strong,{children:"6 fotos aceitas"})," para um teto virar medido, e ele ",(0,s.jsx)(o.strong,{children:"diz por que\nn\xe3o virou"}),". Ent\xe3o o teto absoluto do CHR1 continua sendo ",(0,s.jsx)(o.strong,{children:"fallback publicado"})," (Drillis &\nContini 1966, via Winter), declarado como tal no campo ",(0,s.jsx)(o.code,{children:"procedencia"})," do JSON e na coluna do\nrelat\xf3rio."]}),"\n",(0,s.jsxs)(o.p,{children:["Repare no que isso significa: ter a foto ",(0,s.jsx)(o.strong,{children:"n\xe3o"})," \xe9 ter a medi\xe7\xe3o. Foi mais f\xe1cil aceitar\nque os dados eram ruins do que promover uma medi\xe7\xe3o fr\xe1gil a teto \u2014 e essa \xe9 a Lei 2\naplicada contra o interesse de quem escreveu a r\xe9gua."]}),"\n",(0,s.jsxs)(o.admonition,{type:"warning",children:[(0,s.jsxs)(o.mdxAdmonitionTitle,{children:[(0,s.jsx)(o.code,{children:"references/"})," N\xc3O vem no clone \u2014 e isso \xe9 decis\xe3o, n\xe3o descuido"]}),(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"git ls-files references"})," devolve ",(0,s.jsx)(o.strong,{children:"zero"}),'. Em 04/08/2026 a pasta inteira foi\ndestrackeada por decis\xe3o do dono ("podem ficar local o references porque vamos construir\nlocal"): s\xe3o as telas-alvo da UI e os frames de refer\xeancia do viewmodel, e ficam s\xf3 na\nm\xe1quina dele.']}),(0,s.jsxs)(o.p,{children:["O que ",(0,s.jsx)(o.strong,{children:"sobrevive ao clone s\xe3o os N\xdaMEROS medidos delas"}),": ",(0,s.jsx)(o.code,{children:"tools/eval/ref_ui.json"})," e\n",(0,s.jsx)(o.code,{children:"tools/eval/ref_viewmodel.json"})," est\xe3o versionados. Esse \xe9 o contrato \u2014 se uma r\xe9gua sua\nprecisar rodar em CI, ela l\xea o JSON, nunca o PNG. R\xe9gua que abre imagem de\n",(0,s.jsx)(o.code,{children:"references/"})," fica vermelha em toda m\xe1quina que n\xe3o seja a do dono, e vermelha por\nambiente \xe9 a pior esp\xe9cie: ensina quem trabalha aqui a ignorar vermelho."]})]}),"\n",(0,s.jsx)(o.h2,{id:"teste-de-muta\xe7\xe3o-da-pr\xf3pria-r\xe9gua",children:"Teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua"}),"\n",(0,s.jsx)(o.p,{children:"Esta \xe9 a parte que quase nenhum projeto tem, e \xe9 onde este reposit\xf3rio \xe9 genuinamente\ndiferente."}),"\n",(0,s.jsx)(o.p,{children:(0,s.jsx)(o.strong,{children:"Um quality gate que n\xe3o se mexe quando voc\xea quebra o c\xf3digo de prop\xf3sito est\xe1 cego."})}),"\n",(0,s.jsxs)(o.p,{children:["O jeito de descobrir isso \xe9 mutar: pegue o c\xf3digo corrigido, ",(0,s.jsx)(o.strong,{children:"desfa\xe7a a corre\xe7\xe3o de\nprop\xf3sito"}),", rode o quality gate, e veja se ele fica vermelho. Se ficar verde, o quality gate n\xe3o\nest\xe1 medindo o que voc\xea acha que ele mede."]}),"\n",(0,s.jsx)(o.h3,{id:"o-caso-2022-verde-com-a-corre\xe7\xe3o-removida",children:"O caso: 20/22 verde com a corre\xe7\xe3o removida"}),"\n",(0,s.jsx)(o.p,{children:(0,s.jsxs)(o.strong,{children:["Fonte: ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:910-920"}),"."]})}),"\n",(0,s.jsxs)(o.p,{children:["O contexto: ",(0,s.jsx)(o.code,{children:"public/js/game.js:577"})," declara"]}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-js",children:"const vmOffY = (aspect) => VM_OFF[1] * ((16 / 9) / (aspect || 16 / 9));\n"})}),"\n",(0,s.jsxs)(o.p,{children:["\xc9 a corre\xe7\xe3o de enquadramento vertical por aspecto \u2014 o motivo de a arma ficar no mesmo\nlugar em 16:9 e em 3:2 (o dono joga em 3:2). Ela \xe9 ",(0,s.jsx)(o.strong,{children:"chamada"})," no argumento Y de\n",(0,s.jsx)(o.code,{children:"this.vm.root.position.set(...)"}),", em ",(0,s.jsx)(o.code,{children:"public/js/game.js:4873"}),"."]}),"\n",(0,s.jsx)(o.p,{children:"O buraco, medido em 08/2026:"}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["a etapa ",(0,s.jsx)(o.code,{children:"vmOff"})," conferia s\xf3 ",(0,s.jsx)(o.code,{children:"/this\\.vm\\.root\\.position\\.set\\(\\s*VM_OFF\\[0\\]/"})," \u2014 o termo\nX. O termo Y n\xe3o era conferido por ningu\xe9m, e o auditor (",(0,s.jsx)(o.code,{children:"vm-mint-audit.mjs:196"}),",\n",(0,s.jsx)(o.code,{children:"loadOffYFn"}),") l\xea a ",(0,s.jsx)(o.strong,{children:"DECLARA\xc7\xc3O"})," ",(0,s.jsx)(o.code,{children:"const vmOffY = (aspect) => ..."})," por regex ",(0,s.jsx)(o.strong,{children:"sem nunca\nperguntar se algu\xe9m a CHAMA"}),"."]}),"\n",(0,s.jsxs)(o.p,{children:["Resultado: trocando no ",(0,s.jsx)(o.code,{children:"game.js"})," a chamada ",(0,s.jsx)(o.code,{children:"vmOffY(...)"})," por ",(0,s.jsx)(o.code,{children:"VM_OFF[1]"})," no argumento Y\n\u2014 isto \xe9, ",(0,s.jsx)(o.strong,{children:"removendo por inteiro a corre\xe7\xe3o de enquadramento vertical por aspecto"})," \u2014 o\nquality gate inteiro seguia ",(0,s.jsx)(o.strong,{children:"VERDE (20/22, com VM9, VM10, VM12 e VM15 todas verdes)"}),". Um\nquality gate que n\xe3o distingue o build corrigido do build sem a corre\xe7\xe3o n\xe3o est\xe1 medindo\nnada."]}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["Repare no mecanismo do erro, porque ele se repete em qualquer linguagem: ",(0,s.jsxs)(o.strong,{children:["a invariante\nlia a ",(0,s.jsx)(o.em,{children:"declara\xe7\xe3o"})," de uma constante, e n\xe3o o ",(0,s.jsx)(o.em,{children:"uso"}),"."]})," Declarar e n\xe3o chamar \xe9 o jeito mais\nbarato de uma corre\xe7\xe3o sumir com o quality gate verde."]}),"\n",(0,s.jsxs)(o.p,{children:["O conserto foi cir\xfargico e vale copiar. A AUD1 hoje separa os tr\xeas argumentos do\n",(0,s.jsx)(o.code,{children:"position.set(...)"})," com um ",(0,s.jsx)(o.strong,{children:"varredor de par\xeanteses"})," \u2014 n\xe3o ",(0,s.jsx)(o.code,{children:"split(',')"}),", que cortaria\ndentro da chamada de fun\xe7\xe3o \u2014 e exige ",(0,s.jsx)(o.strong,{children:"nominalmente"})," que o argumento Y chame ",(0,s.jsx)(o.code,{children:"vmOffY("}),".\nE fecha o outro caminho junto (",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:1148-1151"}),"): a f\xf3rmula do\n",(0,s.jsx)(o.code,{children:"vmOffY"})," \xe9 ",(0,s.jsxs)(o.strong,{children:["lida do ",(0,s.jsx)(o.code,{children:"game.js"})," e avaliada"]})," em 16/9, e tem que dar exatamente ",(0,s.jsx)(o.code,{children:"VM_OFF[1]"}),"."]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["Os dois cheques juntos cobrem os dois jeitos de a corre\xe7\xe3o sumir: ",(0,s.jsx)(o.strong,{children:"apagar a CHAMADA"}),"\n(muta\xe7\xe3o medida em 08/2026) ou ",(0,s.jsx)(o.strong,{children:"adulterar a F\xd3RMULA"}),"."]}),"\n"]}),"\n",(0,s.jsx)(o.h3,{id:"n\xe3o-foi-um-caso-isolado--foram-tr\xeas",children:"N\xe3o foi um caso isolado \u2014 foram tr\xeas"}),"\n",(0,s.jsx)(o.p,{children:"O mesmo buraco apareceu em outros dois lugares, e cada um virou uma etapa nova da AUD1:"}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"Muta\xe7\xe3o"}),(0,s.jsx)(o.th,{children:"Placar com a corre\xe7\xe3o desfeita"}),(0,s.jsx)(o.th,{children:"Causa do falso verde"}),(0,s.jsx)(o.th,{children:"Onde"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsxs)(o.td,{children:["Trocar ",(0,s.jsx)(o.code,{children:"vmOffY(...)"})," por ",(0,s.jsx)(o.code,{children:"VM_OFF[1]"})," no argumento Y"]}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.strong,{children:"20/22 verde"})}),(0,s.jsx)(o.td,{children:"a invariante lia a declara\xe7\xe3o, n\xe3o o uso"}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"invariants.mjs:910-920"})})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsxs)(o.td,{children:["Trocar ",(0,s.jsx)(o.code,{children:"g.rotation.set(pit, yaw, t.roll)"})," por ",(0,s.jsx)(o.code,{children:"g.rotation.set(0, 0, t.roll)"})]}),(0,s.jsx)(o.td,{children:"verde"}),(0,s.jsxs)(o.td,{children:["a tabela ",(0,s.jsx)(o.code,{children:"VM_FRAME.cls"})," continua com os \xe2ngulos, e os tr\xeas espelhos continuam batendo ",(0,s.jsx)(o.strong,{children:"entre si"})]}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"invariants.mjs:932-944"})})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsxs)(o.td,{children:["Apagar ",(0,s.jsx)(o.code,{children:"* (weaponCFG(id).vm ?? 1)"})," da escala do mesh"]}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.strong,{children:"28/37 verde, AUD1 inclusive"}),' ("pior \u0394escala 0.0004")']}),(0,s.jsxs)(o.td,{children:["as duas pontas leem ",(0,s.jsx)(o.code,{children:"vm"})," de ",(0,s.jsx)(o.code,{children:"weapons.js"}),"; ",(0,s.jsxs)(o.strong,{children:["o ",(0,s.jsx)(o.code,{children:"game.js"})," nunca \xe9 perguntado"]})," \u2014 era o auditor conferindo a si mesmo"]}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"invariants.mjs:971-975"})})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsxs)(o.td,{children:["Mutar ",(0,s.jsx)(o.code,{children:"this._adsPose['pistol']"})]}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.strong,{children:"20/22 verde"})}),(0,s.jsx)(o.td,{children:"o ADS n\xe3o tinha invariante nenhuma"}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"invariants.mjs:1185"})})]})]})]}),"\n",(0,s.jsx)(o.p,{children:"O padr\xe3o comum das quatro \xe9 o mesmo, e \xe9 o que voc\xea deve procurar na sua invariante:"}),"\n",(0,s.jsx)(o.admonition,{title:"O padr\xe3o do falso verde",type:"danger",children:(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"A r\xe9gua est\xe1 conferindo uma c\xf3pia da regra em vez do jogo."})," Seja porque l\xea a declara\xe7\xe3o\ne n\xe3o o uso, seja porque compara dois espelhos que leem a mesma fonte, seja porque a\ntabela de par\xe2metros continua correta enquanto ningu\xe9m a aplica. Se as duas pontas da sua\ncompara\xe7\xe3o puderem ficar consistentes ",(0,s.jsx)(o.strong,{children:"sem passar pelo c\xf3digo de produ\xe7\xe3o"}),", sua\ninvariante est\xe1 cega."]})}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"tools/eval/mat-check.mjs:18-27"})," resolve isso da forma mais direta poss\xedvel: o corpo do\n",(0,s.jsx)(o.code,{children:"fixVmMaterials"})," \xe9 ",(0,s.jsxs)(o.strong,{children:["recortado do ",(0,s.jsx)(o.code,{children:"game.js"})," e executado"]})," sobre um material-sonda. Se o\nc\xf3digo mudar, a r\xe9gua muda junto. ",(0,s.jsx)(o.em,{children:'"uma r\xe9gua que carrega uma C\xd3PIA da regra mente no dia\nem que a regra muda."'})]}),"\n",(0,s.jsxs)(o.h3,{id:"muta\xe7\xe3o-como-coisa-de-primeira-classe-ui-checkmjs",children:["Muta\xe7\xe3o como coisa de primeira classe: ",(0,s.jsx)(o.code,{children:"ui-check.mjs"})]}),"\n",(0,s.jsxs)(o.p,{children:["O arn\xeas de UI tem uma ",(0,s.jsx)(o.strong,{children:"tabela de muta\xe7\xf5es versionada"}),", e cada uma declara qual quality gate\ntem que ficar vermelho. ",(0,s.jsx)(o.code,{children:"tools/eval/ui-check.mjs:1046-1050"}),":"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["Cada muta\xe7\xe3o DESFAZ um dos consertos desta rodada (ou fura um quality gate de prop\xf3sito) e diz\nqual quality gate TEM que ficar vermelho. ",(0,s.jsx)(o.strong,{children:"Uma r\xe9gua que n\xe3o reprova a vers\xe3o anterior do\npr\xf3prio arquivo n\xe3o \xe9 r\xe9gua, \xe9 decora\xe7\xe3o."})]}),"\n"]}),"\n",(0,s.jsx)(o.p,{children:"Rodar uma:"}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-bash",children:"MUT=ui1_ctf_scrim_fraco node tools/eval/ui-check.mjs # espera UI1 VERMELHA\nMUT=ui3_prompt_na_mira node tools/eval/ui-check.mjs # espera UI3 VERMELHA\nMUT=ui2_prompt_eterno node tools/eval/ui-check.mjs # espera UI2 VERMELHA\nMUT=ui4_ctf_sem_relogio node tools/eval/ui-check.mjs # espera UI4 VERMELHA\n"})}),"\n",(0,s.jsxs)(o.p,{children:["As 7 muta\xe7\xf5es est\xe3o em ",(0,s.jsx)(o.code,{children:"tools/eval/ui-check.mjs:1051-1134"}),". Duas mec\xe2nicas: ",(0,s.jsx)(o.code,{children:"css"})," reescreve\no ",(0,s.jsx)(o.code,{children:"public/style.css"})," ",(0,s.jsx)(o.strong,{children:"lido em mem\xf3ria"})," (nunca em disco \u2014 outros agentes est\xe3o editando o\narquivo agora), e ",(0,s.jsx)(o.code,{children:"sim"})," monkey-patcha o objeto ",(0,s.jsx)(o.code,{children:"Game"})," j\xe1 bootado. Se a muta\xe7\xe3o ",(0,s.jsx)(o.code,{children:"css"})," n\xe3o\ncasar com nada, o script sai com c\xf3digo 2 dizendo ",(0,s.jsx)(o.em,{children:'"o CSS mudou de forma"'})," \u2014 porque uma\nmuta\xe7\xe3o que n\xe3o aplica tamb\xe9m \xe9 um falso verde (",(0,s.jsx)(o.code,{children:"ui-check.mjs:1164"}),")."]}),"\n",(0,s.jsx)(o.h2,{id:"como-escrever-uma-invariante",children:"Como escrever uma invariante"}),"\n",(0,s.jsx)(o.p,{children:"Checklist, na ordem:"}),"\n",(0,s.jsxs)(o.ol,{children:["\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Comece pela frase do defeito."})," Literal, com as palavras de quem reclamou. Todo\narn\xeas desta base come\xe7a assim, e n\xe3o \xe9 estilo: \xe9 o que impede a invariante de medir\noutra coisa. Ver o cabe\xe7alho de ",(0,s.jsx)(o.code,{children:"tools/eval/map-check.mjs:5-12"})," \u2014 cinco frases do dono,\ncinco invariantes."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Traduza para uma grandeza mensur\xe1vel."}),' "os jogadores est\xe3o SUBMERSOS EMBAIXO DA\nEST\xc1TUA" \u2192 ',(0,s.jsx)(o.em,{children:"existe geometria vis\xedvel do mapa cujo topo passa de 0,30 m acima do ch\xe3o\nlocal naquele ponto"})," (MAP1). Note que a defini\xe7\xe3o operacional inclui ",(0,s.jsx)(o.strong,{children:"por que 0,30 m"}),':\n\xe9 o degrau que o corpo sobe; acima disso n\xe3o \xe9 "passar por cima", \xe9 "estar dentro".']}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Ache a proced\xeancia do teto."})," Arquivo de refer\xeancia + pixel medido + script que\nreproduz. Se n\xe3o existir, ",(0,s.jsx)(o.strong,{children:"diga que \xe9 fallback"})," e cite a fonte publicada, como o C1\nfaz. Nunca invente o n\xfamero."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Me\xe7a o c\xf3digo de produ\xe7\xe3o, n\xe3o uma c\xf3pia dele."})," Importe o m\xf3dulo real, recorte a\nfun\xe7\xe3o do arquivo e execute, ou exija nominalmente a chamada no texto do fonte."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Mute e confirme que fica vermelha."})," Desfa\xe7a a corre\xe7\xe3o que voc\xea acabou de fazer e\nrode o quality gate. Se ficar verde, sua invariante est\xe1 cega \u2014 volte pro passo 4. Se der pra\nautomatizar, registre a muta\xe7\xe3o numa tabela, como o ",(0,s.jsx)(o.code,{children:"ui-check.mjs"})," faz."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Escreva a evid\xeancia, n\xe3o s\xf3 o booleano."})," O quarto argumento do ",(0,s.jsx)(o.code,{children:"put()"})," \xe9 o que\nalgu\xe9m vai ler daqui a tr\xeas meses: ",(0,s.jsx)(o.code,{children:'"0,504 a 0,619 da altura em 52 medidas | 0 fora da faixa"'})," \xe9 \xfatil; ",(0,s.jsx)(o.code,{children:'"ok"'})," n\xe3o \xe9."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Escreva o coment\xe1rio de proced\xeancia acima dela."})," Em portugu\xeas, dizendo o que\naconteceu quando o n\xfamero estava errado. \xc9 esse coment\xe1rio que impede a pr\xf3xima rodada\nde refazer o erro."]}),"\n"]}),"\n",(0,s.jsx)(o.h3,{id:"anti-padr\xf5es-que-j\xe1-custaram-caro-aqui",children:"Anti-padr\xf5es que j\xe1 custaram caro aqui"}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"Anti-padr\xe3o"}),(0,s.jsx)(o.th,{children:"O que deu"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Ler a declara\xe7\xe3o de uma constante em vez do uso"}),(0,s.jsx)(o.td,{children:"20/22 verde com a corre\xe7\xe3o removida"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Dois espelhos que leem a mesma fonte"}),(0,s.jsx)(o.td,{children:'28/37 verde, "pior \u0394escala 0.0004", com o knob desligado'})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Adaptador de formato quebrado em sil\xeancio"}),(0,s.jsxs)(o.td,{children:["VM1\u2013VM6 ficaram ",(0,s.jsx)(o.strong,{children:"PULADAS desde que o auditor existe"})," \u2014 6 invariantes de viewmodel que nunca rodaram uma vez (",(0,s.jsx)(o.code,{children:"invariants.mjs:121-127"}),")"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Medir o v\xe3o contra o ch\xe3o local errado"}),(0,s.jsxs)(o.td,{children:["pickup dentro da piscina reportava v\xe3o ",(0,s.jsx)(o.strong,{children:"0,0000 \u2014 VERDE"})," (",(0,s.jsx)(o.code,{children:"pickup-check.mjs:20-23"}),")"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"waypoint \u2264 3 m" como proxy de alcance'}),(0,s.jsxs)(o.td,{children:["74 falsos-positivos e verde em bols\xe3o fechado (",(0,s.jsx)(o.code,{children:"pickup-check.mjs:34-42"}),")"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Piso sem teto"}),(0,s.jsxs)(o.td,{children:['"boca \u2265 0,66" aceita a boca em 0,95 (arma no por\xe3o) \u2014 foi assim que chegamos a 0,816 (',(0,s.jsx)(o.code,{children:"invariants.mjs:432-434"}),")"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Medir num mundo onde o defeito n\xe3o pode acontecer"}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.code,{children:"eval:site"})," cobre ",(0,s.jsx)(o.code,{children:"/ranking"})," e ",(0,s.jsx)(o.strong,{children:"checa corpo"}),", e passou um dia inteiro verde com a p\xe1gina servindo ",(0,s.jsx)(o.strong,{children:"200 com 0 bytes"})," em produ\xe7\xe3o: ele sobe um ",(0,s.jsx)(o.code,{children:"astro dev"})," local, onde ",(0,s.jsx)(o.code,{children:"public/js"})," existe e o ",(0,s.jsx)(o.code,{children:"ENOENT"})," n\xe3o ocorre (BUG-49)"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Aceitar status como prova de p\xe1gina viva"}),(0,s.jsxs)(o.td,{children:["o mesmo BUG-49: ",(0,s.jsx)(o.code,{children:"status === 200"})," chamava de saud\xe1vel uma casca vazia. Corpo agora \xe9 cobrado por ",(0,s.jsx)(o.strong,{children:"tamanho"})]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"N\xfamero medido que ningu\xe9m reprova"}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.code,{children:"gl-metrics.mjs"})," media calls/tri\xe2ngulos desde a rodada 3 e nenhuma cl\xe1usula lia o resultado; o teto s\xf3 existia como prosa num coment\xe1rio. o estacionamento da Loja H (",(0,s.jsx)(o.code,{children:"loja_h"}),") chegou a 4.347 calls antes de algu\xe9m olhar"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Policiar artefato em vez de fonte"}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.code,{children:"mapa-id-check"})," varria ",(0,s.jsx)(o.code,{children:"public/docs/"})," (sa\xedda do Docusaurus) e ficava vermelha quando o bundle publicado estava uma gera\xe7\xe3o atr\xe1s de um rename \u2014 vermelho sem defeito"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"R\xe9gua que se acusa"}),(0,s.jsx)(o.td,{children:"a mesma: ela precisa citar os ids antigos para cobr\xe1-los, e se varria a si mesma. Nove ocorr\xeancias, todas dela"})]})]})]}),"\n",(0,s.jsx)(o.h2,{id:"esta-p\xe1gina-\xe9-a-doutrina-o-passo-a-passo-\xe9-uma-skill",children:"Esta p\xe1gina \xe9 a doutrina. O passo a passo \xe9 uma skill"}),"\n",(0,s.jsxs)(o.p,{children:["O que fazer, na ordem, quando algu\xe9m reporta um defeito \u2014 reproduzir, medir antes de\nconsertar, refutar o palpite \xf3bvio, mutar a r\xe9gua, rodar o quality gate na ordem certa e reportar\no que ",(0,s.jsx)(o.strong,{children:"n\xe3o"})," foi verificado \u2014 est\xe1 em ",(0,s.jsx)(o.code,{children:".claude/skills/bug-hunt/SKILL.md"}),", com o caso real\nque comprou cada regra. Ela \xe9 escrita para agente ",(0,s.jsx)(o.strong,{children:"e"})," para gente, e aponta de volta para\nesta p\xe1gina em vez de repeti-la."]}),"\n",(0,s.jsxs)(o.h2,{id:"port\xf5es-que-n\xe3o-cabem-no-check-e-por-qu\xea",children:["Port\xf5es que N\xc3O cabem no ",(0,s.jsx)(o.code,{children:"check"}),", e por qu\xea"]}),"\n",(0,s.jsxs)(o.p,{children:["Tr\xeas r\xe9guas exigem insumo que o port\xe3o r\xe1pido n\xe3o tem \u2014 navegador, ou o build pronto. Elas\nficam de fora de prop\xf3sito e s\xe3o passo de pr\xe9-deploy, junto do ",(0,s.jsx)(o.code,{children:"eval:boot"}),". Cada uma nasceu\nde um defeito que os port\xf5es existentes n\xe3o podiam ver:"]}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"Comando"}),(0,s.jsx)(o.th,{children:"O que mede"}),(0,s.jsx)(o.th,{children:"O buraco que fechou"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"npm run eval:ssr"})}),(0,s.jsxs)(o.td,{children:["toda p\xe1gina ",(0,s.jsx)(o.code,{children:"prerender = false"})," entrega ",(0,s.jsx)(o.strong,{children:"corpo"}),", medido no artefato do build, entrando no diret\xf3rio da fun\xe7\xe3o (o cwd de produ\xe7\xe3o)"]}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.code,{children:"/mapa"}),", ",(0,s.jsx)(o.code,{children:"/ranking"})," e ",(0,s.jsx)(o.code,{children:"/u/*"})," serviram ",(0,s.jsx)(o.strong,{children:"200 com 0 bytes"})," por um dia. O ",(0,s.jsx)(o.code,{children:"eval:site"})," mede um ",(0,s.jsx)(o.code,{children:"astro dev"})," local, onde o defeito n\xe3o pode acontecer"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"npm run eval:cena"})}),(0,s.jsxs)(o.td,{children:["teto de draw calls e tri\xe2ngulos por frame, ",(0,s.jsx)(o.strong,{children:"por mapa"})]}),(0,s.jsx)(o.td,{children:"o n\xfamero era medido desde a rodada 3 e ningu\xe9m reprovava. Descobriu que o mapa da Quebrada custa 1.8 k calls e roda a metade do fps dos outros"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"npm run eval:mapid"})}),(0,s.jsx)(o.td,{children:"nenhum id no estilo Counter-Strike sobrevive, todo id antigo resolve, e toda pr\xe9via existe em disco"}),(0,s.jsxs)(o.td,{children:["renomear id sem renomear a imagem deixa cartaz quebrado no menu: 404 no navegador, ",(0,s.jsx)(o.strong,{children:"nada"})," no build"]})]})]})]}),"\n",(0,s.jsxs)(o.p,{children:["O teto do ",(0,s.jsx)(o.code,{children:"eval:cena"})," mora em ",(0,s.jsx)(o.code,{children:"tools/eval/cena-tetos.mjs"}),", importado tanto pela r\xe9gua de\nnavegador quanto pelas cl\xe1usulas ",(0,s.jsx)(o.code,{children:"CENA"})," do ",(0,s.jsx)(o.code,{children:"invariants.mjs"})," \u2014 um limiar, dois leitores. Dois\nn\xfameros para o mesmo conceito \xe9 o instrumento discordando de si, e isso j\xe1 custou uma rodada\ninteira aqui."]}),"\n",(0,s.jsx)(o.h2,{id:"rodar-o-quality-gate",children:"Rodar o quality gate"}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-bash",children:"node tools/eval/invariants.mjs # tudo que roda sem browser\nnode tools/eval/invariants.mjs --json # sa\xedda pra m\xe1quina\nnpm run check # syntax + quality gate + vm + coice + bots\nnpm run check:fast # segundos \u2014 rode este primeiro, sempre\n\n# pr\xe9-deploy: exigem navegador ou build, e por isso ficam fora dos de cima\nnpm run eval:boot # o jogo ABRE?\nnpm run build && npm run eval:ssr # p\xe1gina SSR entrega corpo?\nnpm run eval:cena # custo de cena dentro do teto?\n"})}),"\n",(0,s.jsxs)(o.p,{children:["Fontes atuais de produ\xe7\xe3o, dados e d\xedvida conhecida: ",(0,s.jsx)(o.a,{href:"/docs/estado",children:"Estado atual"}),"."]})]})}function m(e={}){const{wrapper:o}={...(0,n.R)(),...e.components};return o?(0,s.jsx)(o,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},8453(e,o,a){a.d(o,{R:()=>i,x:()=>d});var r=a(6540);const s={},n=r.createContext(s);function i(e){const o=r.useContext(n);return r.useMemo(function(){return"function"==typeof e?e(o):{...o,...e}},[o,e])}function d(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),r.createElement(n.Provider,{value:o},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[475],{1669(e,o,a){a.r(o),a.d(o,{assets:()=>c,contentTitle:()=>d,default:()=>m,frontMatter:()=>i,metadata:()=>r,toc:()=>t});const r=JSON.parse('{"id":"quality-gates","title":"O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o","description":"O que \xe9 uma invariante neste repo, como se escreve uma, as duas leis da casa com o caso real de cada uma, e o teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua.","source":"@site/docs/quality-gates.md","sourceDirName":".","slug":"/quality-gates","permalink":"/docs/quality-gates","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/quality-gates.md","tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"id":"quality-gates","title":"O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o","sidebar_label":"Quality gates","sidebar_position":4,"description":"O que \xe9 uma invariante neste repo, como se escreve uma, as duas leis da casa com o caso real de cada uma, e o teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua."},"sidebar":"dev","previous":{"title":"Instrumenta\xe7\xe3o de IA","permalink":"/docs/instrumentacao-ai"},"next":{"title":"BotBrain","permalink":"/docs/botbrain"}}');var s=a(4848),n=a(8453);const i={id:"quality-gates",title:"O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o",sidebar_label:"Quality gates",sidebar_position:4,description:"O que \xe9 uma invariante neste repo, como se escreve uma, as duas leis da casa com o caso real de cada uma, e o teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua."},d="O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o",c={},t=[{value:"Por que ele existe",id:"por-que-ele-existe",level:2},{value:"O que \xe9 uma invariante aqui",id:"o-que-\xe9-uma-invariante-aqui",level:2},{value:"Severidade",id:"severidade",level:3},{value:"As duas leis da casa",id:"as-duas-leis-da-casa",level:2},{value:"Lei 1 \u2014 Inten\xe7\xe3o que n\xe3o vira invariante \xe9 otimizada para fora",id:"lei-1--inten\xe7\xe3o-que-n\xe3o-vira-invariante-\xe9-otimizada-para-fora",level:3},{value:"Lei 2 \u2014 Teto sem proced\xeancia \xe9 opini\xe3o",id:"lei-2--teto-sem-proced\xeancia-\xe9-opini\xe3o",level:3},{value:"Teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua",id:"teste-de-muta\xe7\xe3o-da-pr\xf3pria-r\xe9gua",level:2},{value:"O caso: 20/22 verde com a corre\xe7\xe3o removida",id:"o-caso-2022-verde-com-a-corre\xe7\xe3o-removida",level:3},{value:"N\xe3o foi um caso isolado \u2014 foram tr\xeas",id:"n\xe3o-foi-um-caso-isolado--foram-tr\xeas",level:3},{value:"Muta\xe7\xe3o como coisa de primeira classe: ui-check.mjs",id:"muta\xe7\xe3o-como-coisa-de-primeira-classe-ui-checkmjs",level:3},{value:"Como escrever uma invariante",id:"como-escrever-uma-invariante",level:2},{value:"Anti-padr\xf5es que j\xe1 custaram caro aqui",id:"anti-padr\xf5es-que-j\xe1-custaram-caro-aqui",level:3},{value:"Esta p\xe1gina \xe9 a doutrina. O passo a passo \xe9 uma skill",id:"esta-p\xe1gina-\xe9-a-doutrina-o-passo-a-passo-\xe9-uma-skill",level:2},{value:"Port\xf5es que N\xc3O cabem no check, e por qu\xea",id:"port\xf5es-que-n\xe3o-cabem-no-check-e-por-qu\xea",level:2},{value:"Rodar o quality gate",id:"rodar-o-quality-gate",level:2}];function l(e){const o={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,n.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(o.header,{children:(0,s.jsx)(o.h1,{id:"o-quality-gate-invariantes-proced\xeancia-e-muta\xe7\xe3o",children:"O quality gate: invariantes, proced\xeancia e muta\xe7\xe3o"})}),"\n",(0,s.jsxs)(o.p,{children:["O quality gate deste reposit\xf3rio \xe9 um arquivo: ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs"}),". Ele roda em node puro\ne sai com c\xf3digo 1 se qualquer invariante ",(0,s.jsx)(o.strong,{children:"cr\xedtica"})," falhar. \xc9 o que o CI executa em todo\nPR (",(0,s.jsx)(o.code,{children:".github/workflows/ci.yml"}),")."]}),"\n","\n",(0,s.jsxs)(o.ul,{children:["\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs"}),": ",(0,s.jsx)(o.strong,{children:"2.275 linhas"}),", ",(0,s.jsx)(o.strong,{children:"65 identificadores de invariante declarados"})," (",(0,s.jsx)(o.code,{children:"put()"}),"), dos quais ",(0,s.jsx)(o.strong,{children:"28"})," t\xeam caminho de ",(0,s.jsx)(o.code,{children:"skip()"})," declarado."]}),"\n",(0,s.jsxs)(o.li,{children:["O arn\xeas inteiro s\xe3o ",(0,s.jsx)(o.strong,{children:"200 scripts"})," em ",(0,s.jsx)(o.code,{children:"tools/eval/"})," (",(0,s.jsx)(o.code,{children:".mjs"})," + ",(0,s.jsx)(o.code,{children:".py"}),"), mais ",(0,s.jsx)(o.strong,{children:"54 scripts"})," de pipeline em ",(0,s.jsx)(o.code,{children:"tools/"}),"."]}),"\n",(0,s.jsxs)(o.li,{children:["Quantas invariantes rodam como ",(0,s.jsx)(o.strong,{children:"cr\xedticas"})," numa execu\xe7\xe3o ",(0,s.jsx)(o.strong,{children:"n\xe3o \xe9 deriv\xe1vel do fonte"}),": depende de qual insumo existe na m\xe1quina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse n\xfamero s\xf3 sai rodando o quality gate \u2014 e o lugar dele \xe9 o cabe\xe7alho do ",(0,s.jsx)(o.code,{children:"KNOWN-BUGS.md"}),", atualizado com sa\xedda real."]}),"\n"]}),"\n",(0,s.jsx)(o.p,{children:"Reproduza:"}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-bash",children:"grep -o \"put('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l\ngrep -o \"skip('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l\n"})}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["Bloco gerado por ",(0,s.jsx)(o.code,{children:"node tools/gen-docs.mjs"}),". Fonte: ",(0,s.jsx)(o.code,{children:"grep -o \"put('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l"})]}),"\n"]}),"\n","\n",(0,s.jsxs)(o.p,{children:["O terceiro item acima \xe9 a distin\xe7\xe3o que mais confunde quem chega: ",(0,s.jsx)(o.strong,{children:"identificador\ndeclarado \u2260 invariante avaliada."})," V\xe1rias viram ",(0,s.jsx)(o.code,{children:"skip"})," em vez de ",(0,s.jsx)(o.code,{children:"put"})," quando falta o\ninsumo delas (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). ",(0,s.jsx)(o.code,{children:"skip"})," \xe9\n",(0,s.jsx)(o.strong,{children:"quality gate verde por aus\xeancia de dado"}),', e \xe9 por isso que ele sempre carrega o motivo. Ver\n"Severidade", abaixo.']}),"\n",(0,s.jsx)(o.p,{children:"Esta p\xe1gina \xe9 a mais \xfatil do site. Se voc\xea s\xf3 for ler uma, leia esta."}),"\n",(0,s.jsx)(o.h2,{id:"por-que-ele-existe",children:"Por que ele existe"}),"\n",(0,s.jsxs)(o.p,{children:["Do cabe\xe7alho do pr\xf3prio arquivo, ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:5-19"}),":"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["O dono passou 3 dias num ciclo em que cada rodada consertava uma coisa e quebrava\noutra, e a gente s\xf3 descobria uma rodada depois. A causa n\xe3o era falta de cuidado:\nera falta de R\xc9GUA. Um cr\xedtico (humano ou agente) julga screenshot; consist\xeancia e\nflow s\xe3o propriedades do jogo ",(0,s.jsx)(o.strong,{children:"EM MOVIMENTO"}),", e quase todo defeito que ele reportou\nn\xe3o \xe9 gosto \u2014 \xe9 invariante violada."]}),"\n"]}),"\n",(0,s.jsx)(o.p,{children:"E a tradu\xe7\xe3o, que \xe9 a coisa mais importante deste reposit\xf3rio inteiro:"}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"O que o dono disse"}),(0,s.jsx)(o.th,{children:"Qual invariante isso virou"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"as m\xe3os est\xe3o soltas no ar"'}),(0,s.jsx)(o.td,{children:"dist\xe2ncia m\xe3o\u2194grip tem um teto"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"a arma aponta pra baixo"'}),(0,s.jsx)(o.td,{children:"o cano tem um \xe2ngulo m\xe1ximo"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"no ADS n\xe3o vejo a arma nem a mira"'}),(0,s.jsx)(o.td,{children:"a arma tem \xe1rea m\xednima e m\xe1xima"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"sniper sem zoom"'}),(0,s.jsx)(o.td,{children:"FOV mirando < FOV de quadril"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"v\xe1rias armas com visual igual"'}),(0,s.jsx)(o.td,{children:"silhuetas t\xeam que diferir"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"o bot atira do nada"'}),(0,s.jsx)(o.td,{children:"dano exige LOS anterior"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"tem 2 me eliminando"'}),(0,s.jsx)(o.td,{children:"1 killfeed por morte"})]})]})]}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:20-21"}),":"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsx)(o.p,{children:(0,s.jsx)(o.strong,{children:"REGRA DE OURO: nada \xe9 commitado com invariante VERMELHA. E todo bug novo que o dono\nreportar vira uma invariante aqui \u2014 \xe9 assim que ele nunca volta."})}),"\n"]}),"\n",(0,s.jsx)(o.h2,{id:"o-que-\xe9-uma-invariante-aqui",children:"O que \xe9 uma invariante aqui"}),"\n",(0,s.jsxs)(o.p,{children:["Uma invariante \xe9 uma ",(0,s.jsx)(o.strong,{children:"propriedade do jogo que d\xe1 pra medir sem um humano olhando"}),", com\num teto ou uma faixa que tem proced\xeancia. N\xe3o \xe9 teste unit\xe1rio: quase nenhuma invariante\ntesta uma fun\xe7\xe3o. Elas medem o ",(0,s.jsx)(o.strong,{children:"estado do jogo rodando de verdade"}),"."]}),"\n",(0,s.jsx)(o.p,{children:"Tr\xeas formas, todas presentes no arquivo:"}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"1. Lida do c\xf3digo-fonte."})," Barata, roda em milissegundos, pega classes inteiras de bug.\nExemplo real, ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:1439-1446"}),":"]}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-js",children:"// ARM1 \u2014 toda arma com luneta precisa de zoom de verdade. \"Snipers sem zoom\"\n// \xe9 reclama\xe7\xe3o literal; a solu\xe7\xe3o N\xc3O \xe9 tirar a luneta, \xe9 fazer a certa.\nconst bloco = gsrc.slice(0, gsrc.indexOf('};', gsrc.indexOf('const WEAPONS')) + 2);\nconst linhas = bloco.split('\\n').filter((l) => /^\\s*\\w+:\\s*\\{/.test(l));\nconst semZoom = linhas.filter((l) => /scope:\\s*true/.test(l) && !/spreadScope/.test(l))\n .map((l) => l.trim().split(':')[0]);\nput('ARM1', 'toda arma com scope:true declara spreadScope', semZoom.length === 0,\n semZoom.length ? semZoom.join(', ') : `${linhas.length} armas conferidas`);\n"})}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"2. Medida no jogo real rodando em node."})," ",(0,s.jsx)(o.code,{children:"tools/eval/harness.mjs"})," sobe a classe ",(0,s.jsx)(o.code,{children:"Game"}),"\nde verdade, com os mapas de verdade, com DOM/canvas stubado. \xc9 o ",(0,s.jsx)(o.strong,{children:"c\xf3digo de produ\xe7\xe3o"}),"\nque \xe9 medido, n\xe3o uma reimplementa\xe7\xe3o \u2014 ",(0,s.jsx)(o.code,{children:"tools/eval/botsim.mjs:8-9"}),": ",(0,s.jsx)(o.em,{children:'"se o n\xfamero\nmelhorar aqui, melhorou no jogo"'}),". Daqui saem BOT1\u2013BOT8, MAP1\u2013MAP3, CTF1, MAT1/MAT2,\nFOG1, TEX1, VM14, MOD1/MOD2."]}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"3. Medida na geometria dos assets."})," ",(0,s.jsx)(o.code,{children:"vm-mint-audit.mjs"})," abre todos os GLBs de arma com um\nparser de GLB pr\xf3prio e projeta o viewmodel na tela. Daqui saem VM1\u2013VM19."]}),"\n",(0,s.jsxs)(o.p,{children:["O que ",(0,s.jsx)(o.strong,{children:"n\xe3o"})," cabe aqui: invariante que exige pixel de browser. Essas est\xe3o marcadas\n",(0,s.jsx)(o.code,{children:"browser"})," e s\xe3o puladas, com o motivo dito \u2014 SwiftShader custa ~4 min por carga de mapa\nnesta m\xe1quina (",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:99"}),")."]}),"\n",(0,s.jsx)(o.h3,{id:"severidade",children:"Severidade"}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"put(id, desc, ok, evid, sev)"})," aceita ",(0,s.jsx)(o.code,{children:"'crit'"})," (padr\xe3o) ou ",(0,s.jsx)(o.code,{children:"'warn'"}),"\n(",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:81-82"}),"). Cr\xedtica vermelha reprova o PR. Warn \xe9 ru\xeddo medido que\nalgu\xe9m precisa olhar mas n\xe3o bloqueia \u2014 \xe9 onde vivem BOT1/BOT2/BOT3/BOT6/BOT7, ARM4 e\nARM5. ",(0,s.jsx)(o.code,{children:"skip()"})," \xe9 o terceiro estado, e ele \xe9 ",(0,s.jsx)(o.strong,{children:"perigoso"}),": quality gate verde por aus\xeancia de\ndado. Por isso todo ",(0,s.jsx)(o.code,{children:"skip"})," carrega o motivo."]}),"\n",(0,s.jsx)(o.h2,{id:"as-duas-leis-da-casa",children:"As duas leis da casa"}),"\n",(0,s.jsx)(o.h3,{id:"lei-1--inten\xe7\xe3o-que-n\xe3o-vira-invariante-\xe9-otimizada-para-fora",children:"Lei 1 \u2014 Inten\xe7\xe3o que n\xe3o vira invariante \xe9 otimizada para fora"}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsxs)(o.strong,{children:["Fonte: ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:452-461"}),"."]})," O caso, literal:"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["a rodada anterior levou o quality gate de ",(0,s.jsx)(o.strong,{children:"16/21 para 19/21 sem afrouxar um teto sequer"})," e\nmesmo assim foi ",(0,s.jsx)(o.strong,{children:"REPROVADA"})," pelo dono, porque para fechar VM5/VM10 ela ",(0,s.jsxs)(o.strong,{children:["ZEROU o\n",(0,s.jsx)(o.code,{children:"VM_OFF"})," y"]}),' e mudou o look em sil\xeancio. Nenhuma invariante codificava "onde fica a\nboca do cano", ent\xe3o a m\xe9trica foi otimizada e a INTEN\xc7\xc3O foi destru\xedda. Lei de\nGoodhart, na \xedntegra. ',(0,s.jsx)(o.strong,{children:"INTEN\xc7\xc3O QUE N\xc3O VIRA INVARIANTE \xc9 OTIMIZADA PARA FORA."})]}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["Leia de novo o que aconteceu, porque \xe9 contraintuitivo: o agente ",(0,s.jsx)(o.strong,{children:"n\xe3o trapaceou"}),". Ele\nn\xe3o afrouxou nenhum teto. Ele subiu o placar de verdade. E o resultado foi pior, porque\n",(0,s.jsx)(o.code,{children:"VM_OFF[1]"})," \xe9 o termo que ",(0,s.jsx)(o.strong,{children:"domina a posi\xe7\xe3o da arma na tela"})," \u2014 ",(0,s.jsx)(o.code,{children:"public/js/game.js:555"}),"\ndeclara ",(0,s.jsx)(o.code,{children:"VM_OFF = [0.03, -0.1000, 0]"}),", e ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:1163"})," mede a\nsensibilidade: ",(0,s.jsx)(o.em,{children:'"tirar o recuoZ move o grip 3,5 cm; tirar o VM_OFF move 23 cm"'}),"."]}),"\n",(0,s.jsx)(o.p,{children:"Zerar esse termo fechou duas invariantes e apagou a decis\xe3o est\xe9tica que o dono tinha\ntomado \u2014 que n\xe3o estava escrita em lugar nenhum que a r\xe9gua pudesse ler."}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"A corre\xe7\xe3o n\xe3o foi punir o agente. Foi escrever a inten\xe7\xe3o como invariante."})," Hoje\nexiste a VM12 (",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:497"}),"): ",(0,s.jsx)(o.em,{children:'"look CS 1.6: boca do cano LOGO abaixo\nda mira (y entre 0,50 e 0,62) nos 2 aspectos"'}),". Com ela no lugar, a mesma otimiza\xe7\xe3o\nfica ",(0,s.jsx)(o.strong,{children:"vermelha"}),"."]}),"\n",(0,s.jsx)(o.p,{children:"E a consequ\xeancia operacional, do mesmo coment\xe1rio:"}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["Quem quiser mudar o look tem que mudar ",(0,s.jsx)(o.strong,{children:"ESTE teto"})," explicitamente, num diff que o\ndono v\xea, em vez de mexer no ",(0,s.jsx)(o.code,{children:"VM_OFF"}),' e reportar "+3 invariantes".']}),"\n"]}),"\n",(0,s.jsx)(o.admonition,{title:"O que isso significa pro seu PR",type:"tip",children:(0,s.jsxs)(o.p,{children:["Se a sua mudan\xe7a melhora o placar do quality gate, a primeira pergunta \xe9: ",(0,s.jsx)(o.strong,{children:"o que eu mudei que\no quality gate n\xe3o olha?"}),' Se a resposta for "o look", "o feel" ou "a sensa\xe7\xe3o", escreva a\ninvariante antes de mandar o PR \u2014 ou explique no PR por que ela n\xe3o cabe.']})}),"\n",(0,s.jsx)(o.h3,{id:"lei-2--teto-sem-proced\xeancia-\xe9-opini\xe3o",children:"Lei 2 \u2014 Teto sem proced\xeancia \xe9 opini\xe3o"}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsxs)(o.strong,{children:["Fonte: ",(0,s.jsx)(o.code,{children:"tools/eval/ref-measure.py:1-40"}),"."]})," Essa docstring \xe9 a doutrina da casa. O caso:"]}),"\n",(0,s.jsxs)(o.p,{children:["Durante ",(0,s.jsx)(o.strong,{children:"tr\xeas dias"})," o quality gate de armas foi resolvido contra n\xfameros ",(0,s.jsx)(o.strong,{children:"asseridos"}),":"]}),"\n",(0,s.jsxs)(o.ul,{children:["\n",(0,s.jsxs)(o.li,{children:["A VM12 exigia ",(0,s.jsx)(o.em,{children:'"boca do cano em y \u2265 0,66"'}),"."]}),"\n",(0,s.jsxs)(o.li,{children:["O doc do ",(0,s.jsx)(o.code,{children:"vmattach.js"})," dizia ",(0,s.jsx)(o.em,{children:'"coronha INTEIRA no canto"'}),"."]}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["Nenhum dos dois foi medido em imagem nenhuma. Segundo ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:461-463"}),",\no piso 0,66 veio de um coment\xe1rio do ",(0,s.jsx)(o.code,{children:"public/js/vmattach.js"})," \u2014 ",(0,s.jsx)(o.em,{children:'"a boca fica a ~0,66H"'})," \u2014\nque por sua vez veio de um v\xeddeo assistido. (O coment\xe1rio do quality gate aponta para\n",(0,s.jsx)(o.code,{children:"vmattach.js:387-392"}),"; hoje o texto est\xe1 em ",(0,s.jsx)(o.code,{children:"vmattach.js:395"}),", porque o arquivo andou. \xc9\nexatamente o motivo de o ",(0,s.jsx)(o.code,{children:"ARCH.md"})," ser gerado \u2014 ver ",(0,s.jsx)(o.a,{href:"/docs/arquitetura",children:"Arquitetura"}),".)"]}),"\n",(0,s.jsxs)(o.p,{children:["O dono olhou o resultado e disse, literal (",(0,s.jsx)(o.code,{children:"ref-measure.py:14-17"}),"):"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsx)(o.p,{children:'"est\xe1 diferente do CS 1.6 e do Quake e do UT; nesses 3 a arma est\xe1 sempre no canto\ninferior direito e a coronha sempre FORA; depois de 3 dias e uma pasta inteira de\nrefer\xeancia nem voc\xea nem o Kimi entendeu isso."'}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["A\xed os frames foram ",(0,s.jsx)(o.strong,{children:"medidos"}),". ",(0,s.jsx)(o.code,{children:"tools/eval/ref-measure.py"})," faz segmenta\xe7\xe3o por cor no\nquadrante inferior-direito, pega a maior componente conexa, e escreve\n",(0,s.jsx)(o.code,{children:"tools/eval/ref_viewmodel.json"}),". Resultado:"]}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"Frame"}),(0,s.jsx)(o.th,{children:"Boca (x, y)"}),(0,s.jsx)(o.th,{style:{textAlign:"right"},children:"\xc1rea na tela"}),(0,s.jsx)(o.th,{style:{textAlign:"right"},children:"\xc2ngulo do eixo"}),(0,s.jsx)(o.th,{children:"Cruza a borda direita?"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"cs16_ak_dust.jpg"})}),(0,s.jsxs)(o.td,{children:["0,564 ; ",(0,s.jsx)(o.strong,{children:"0,513"})]}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"9,76%"}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"28,0\xb0"}),(0,s.jsx)(o.td,{children:"sim"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"cs16_m4_dust.jpg"})}),(0,s.jsxs)(o.td,{children:["0,569 ; ",(0,s.jsx)(o.strong,{children:"0,598"})]}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"9,78%"}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"34,8\xb0"}),(0,s.jsx)(o.td,{children:"sim"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"valorant_vandal.jpg"})}),(0,s.jsxs)(o.td,{children:["0,648 ; ",(0,s.jsx)(o.strong,{children:"0,587"})]}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"13,09%"}),(0,s.jsx)(o.td,{style:{textAlign:"right"},children:"4,6\xb0"}),(0,s.jsx)(o.td,{children:"sim"})]})]})]}),"\n",(0,s.jsx)(o.p,{children:(0,s.jsx)(o.strong,{children:"Os dois n\xfameros asseridos estavam errados:"})}),"\n",(0,s.jsxs)(o.ol,{children:["\n",(0,s.jsxs)(o.li,{children:["A boca do CS 1.6 fica em ",(0,s.jsx)(o.strong,{children:"0,513\u20130,598"})," \u2014 logo abaixo da mira (0,5), 1 a 10 pontos\npercentuais abaixo do centro. N\xe3o em 0,66\u20130,93. O piso errado estava mantendo a nossa\narma ",(0,s.jsx)(o.strong,{children:"afundada"})," em 0,667\u20130,816 (",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:472-475"}),")."]}),"\n",(0,s.jsxs)(o.li,{children:["A coronha ",(0,s.jsx)(o.strong,{children:"SAI pela quina"})," nos 3 frames. Sair \xe9 o padr\xe3o, n\xe3o o defeito\n(",(0,s.jsx)(o.code,{children:"ref_viewmodel.json"})," \u2192 ",(0,s.jsx)(o.code,{children:"faixas.cruzaBordaDireita: true"})," nos 3)."]}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["E o dano colateral: com o teto falso, o solver da rodada anterior ",(0,s.jsx)(o.em,{children:'"provou"'})," que 3% de\n\xe1rea era invi\xe1vel. A prova estava certa ",(0,s.jsx)(o.strong,{children:"contra aquele teto"})," \u2014 e o teto \xe9 que era falso\n(",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:476-478"}),")."]}),"\n",(0,s.jsxs)(o.p,{children:["A regra que ficou, ",(0,s.jsx)(o.code,{children:"tools/eval/ref-measure.py:21-22"}),":"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsx)(o.p,{children:(0,s.jsx)(o.strong,{children:"TETO DE INVARIANTE S\xd3 ENTRA COM PROCED\xcaNCIA \u2014 arquivo de refer\xeancia, pixel medido, e\neste script reproduzindo o n\xfamero. N\xfamero sem imagem \xe9 opini\xe3o."})}),"\n"]}),"\n",(0,s.jsx)(o.p,{children:"Hoje as invariantes de enquadramento carregam a proced\xeancia no pr\xf3prio texto: VM1 (faixa\n0,50\u20130,60, ref 0,520\u20130,565), VM3 (22\u201342\xb0, ref 28,0\xb0 e 34,8\xb0), VM5 (6\u201316%, ref\n9,76\u201313,09%), VM12 (0,50\u20130,62, ref 0,513\u20130,598), VM16 (fatia na borda direita 0,02\u20130,20,\nref 0,053\u20130,095)."}),"\n",(0,s.jsx)(o.admonition,{title:"Proced\xeancia inclui admitir o que a imagem N\xc3O mede",type:"note",children:(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:599-602"}),' recusa criar um teto para "quanto da arma fica fora\ndo quadro", porque o que est\xe1 fora \xe9 invis\xedvel na foto \u2014 n\xe3o d\xe1 pra saber se a coronha do\nAK termina 5 cm ou 50 cm al\xe9m da borda. Os n\xfameros continuam no JSON como ',(0,s.jsx)(o.strong,{children:"evid\xeancia,\nsem gate"}),". Isso \xe9 proced\xeancia levada a s\xe9rio: a r\xe9gua diz onde ela para de saber."]})}),"\n",(0,s.jsxs)(o.p,{children:["E o mesmo rigor morde quem escreveu a r\xe9gua, no caso mais desconfort\xe1vel poss\xedvel: ",(0,s.jsx)(o.strong,{children:"as\nfotos de refer\xeancia de personagem chegaram, foram medidas, e foram REPROVADAS pela pr\xf3pria\nr\xe9gua."})," ",(0,s.jsx)(o.code,{children:"tools/eval/char-probe.mjs:25-45"})," conta o epis\xf3dio inteiro \u2014 ",(0,s.jsx)(o.code,{children:"references/funkeiros/"}),"\ntem 23 arquivos e ",(0,s.jsx)(o.code,{children:"references/palhacos/"})," tem 21, todos passados pelo ",(0,s.jsx)(o.code,{children:"ref-body.py"}),", com as\nm\xe1scaras ",(0,s.jsx)(o.strong,{children:"olhadas"})," (",(0,s.jsx)(o.code,{children:"--masks"}),"). O veredito, dito na cara pelo pr\xf3prio coment\xe1rio: s\xe3o\nselfies e closes; a segmenta\xe7\xe3o heur\xedstica devolve a m\xe3o, um peda\xe7o de jaqueta ou o cabelo\nde outra pessoa no fundo, e a raz\xe3o ombro/altura sai entre ",(0,s.jsx)(o.strong,{children:"0,42 e 3,78"})," quando um humano\nmede 0,259. Sobra ~1 foto de corpo inteiro utiliz\xe1vel \u2014 n\xe3o \xe9 amostra."]}),"\n",(0,s.jsxs)(o.p,{children:["O ",(0,s.jsx)(o.code,{children:"ref-body.py"})," exige ",(0,s.jsx)(o.strong,{children:"6 fotos aceitas"})," para um teto virar medido, e ele ",(0,s.jsx)(o.strong,{children:"diz por que\nn\xe3o virou"}),". Ent\xe3o o teto absoluto do CHR1 continua sendo ",(0,s.jsx)(o.strong,{children:"fallback publicado"})," (Drillis &\nContini 1966, via Winter), declarado como tal no campo ",(0,s.jsx)(o.code,{children:"procedencia"})," do JSON e na coluna do\nrelat\xf3rio."]}),"\n",(0,s.jsxs)(o.p,{children:["Repare no que isso significa: ter a foto ",(0,s.jsx)(o.strong,{children:"n\xe3o"})," \xe9 ter a medi\xe7\xe3o. Foi mais f\xe1cil aceitar\nque os dados eram ruins do que promover uma medi\xe7\xe3o fr\xe1gil a teto \u2014 e essa \xe9 a Lei 2\naplicada contra o interesse de quem escreveu a r\xe9gua."]}),"\n",(0,s.jsxs)(o.admonition,{type:"warning",children:[(0,s.jsxs)(o.mdxAdmonitionTitle,{children:[(0,s.jsx)(o.code,{children:"references/"})," N\xc3O vem no clone \u2014 e isso \xe9 decis\xe3o, n\xe3o descuido"]}),(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"git ls-files references"})," devolve ",(0,s.jsx)(o.strong,{children:"zero"}),'. Em 04/08/2026 a pasta inteira foi\ndestrackeada por decis\xe3o do dono ("podem ficar local o references porque vamos construir\nlocal"): s\xe3o as telas-alvo da UI e os frames de refer\xeancia do viewmodel, e ficam s\xf3 na\nm\xe1quina dele.']}),(0,s.jsxs)(o.p,{children:["O que ",(0,s.jsx)(o.strong,{children:"sobrevive ao clone s\xe3o os N\xdaMEROS medidos delas"}),": ",(0,s.jsx)(o.code,{children:"tools/eval/ref_ui.json"})," e\n",(0,s.jsx)(o.code,{children:"tools/eval/ref_viewmodel.json"})," est\xe3o versionados. Esse \xe9 o contrato \u2014 se uma r\xe9gua sua\nprecisar rodar em CI, ela l\xea o JSON, nunca o PNG. R\xe9gua que abre imagem de\n",(0,s.jsx)(o.code,{children:"references/"})," fica vermelha em toda m\xe1quina que n\xe3o seja a do dono, e vermelha por\nambiente \xe9 a pior esp\xe9cie: ensina quem trabalha aqui a ignorar vermelho."]})]}),"\n",(0,s.jsx)(o.h2,{id:"teste-de-muta\xe7\xe3o-da-pr\xf3pria-r\xe9gua",children:"Teste de muta\xe7\xe3o da pr\xf3pria r\xe9gua"}),"\n",(0,s.jsx)(o.p,{children:"Esta \xe9 a parte que quase nenhum projeto tem, e \xe9 onde este reposit\xf3rio \xe9 genuinamente\ndiferente."}),"\n",(0,s.jsx)(o.p,{children:(0,s.jsx)(o.strong,{children:"Um quality gate que n\xe3o se mexe quando voc\xea quebra o c\xf3digo de prop\xf3sito est\xe1 cego."})}),"\n",(0,s.jsxs)(o.p,{children:["O jeito de descobrir isso \xe9 mutar: pegue o c\xf3digo corrigido, ",(0,s.jsx)(o.strong,{children:"desfa\xe7a a corre\xe7\xe3o de\nprop\xf3sito"}),", rode o quality gate, e veja se ele fica vermelho. Se ficar verde, o quality gate n\xe3o\nest\xe1 medindo o que voc\xea acha que ele mede."]}),"\n",(0,s.jsx)(o.h3,{id:"o-caso-2022-verde-com-a-corre\xe7\xe3o-removida",children:"O caso: 20/22 verde com a corre\xe7\xe3o removida"}),"\n",(0,s.jsx)(o.p,{children:(0,s.jsxs)(o.strong,{children:["Fonte: ",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:910-920"}),"."]})}),"\n",(0,s.jsxs)(o.p,{children:["O contexto: ",(0,s.jsx)(o.code,{children:"public/js/game.js:577"})," declara"]}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-js",children:"const vmOffY = (aspect) => VM_OFF[1] * ((16 / 9) / (aspect || 16 / 9));\n"})}),"\n",(0,s.jsxs)(o.p,{children:["\xc9 a corre\xe7\xe3o de enquadramento vertical por aspecto \u2014 o motivo de a arma ficar no mesmo\nlugar em 16:9 e em 3:2 (o dono joga em 3:2). Ela \xe9 ",(0,s.jsx)(o.strong,{children:"chamada"})," no argumento Y de\n",(0,s.jsx)(o.code,{children:"this.vm.root.position.set(...)"}),", em ",(0,s.jsx)(o.code,{children:"public/js/game.js:4873"}),"."]}),"\n",(0,s.jsx)(o.p,{children:"O buraco, medido em 08/2026:"}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["a etapa ",(0,s.jsx)(o.code,{children:"vmOff"})," conferia s\xf3 ",(0,s.jsx)(o.code,{children:"/this\\.vm\\.root\\.position\\.set\\(\\s*VM_OFF\\[0\\]/"})," \u2014 o termo\nX. O termo Y n\xe3o era conferido por ningu\xe9m, e o auditor (",(0,s.jsx)(o.code,{children:"vm-mint-audit.mjs:196"}),",\n",(0,s.jsx)(o.code,{children:"loadOffYFn"}),") l\xea a ",(0,s.jsx)(o.strong,{children:"DECLARA\xc7\xc3O"})," ",(0,s.jsx)(o.code,{children:"const vmOffY = (aspect) => ..."})," por regex ",(0,s.jsx)(o.strong,{children:"sem nunca\nperguntar se algu\xe9m a CHAMA"}),"."]}),"\n",(0,s.jsxs)(o.p,{children:["Resultado: trocando no ",(0,s.jsx)(o.code,{children:"game.js"})," a chamada ",(0,s.jsx)(o.code,{children:"vmOffY(...)"})," por ",(0,s.jsx)(o.code,{children:"VM_OFF[1]"})," no argumento Y\n\u2014 isto \xe9, ",(0,s.jsx)(o.strong,{children:"removendo por inteiro a corre\xe7\xe3o de enquadramento vertical por aspecto"})," \u2014 o\nquality gate inteiro seguia ",(0,s.jsx)(o.strong,{children:"VERDE (20/22, com VM9, VM10, VM12 e VM15 todas verdes)"}),". Um\nquality gate que n\xe3o distingue o build corrigido do build sem a corre\xe7\xe3o n\xe3o est\xe1 medindo\nnada."]}),"\n"]}),"\n",(0,s.jsxs)(o.p,{children:["Repare no mecanismo do erro, porque ele se repete em qualquer linguagem: ",(0,s.jsxs)(o.strong,{children:["a invariante\nlia a ",(0,s.jsx)(o.em,{children:"declara\xe7\xe3o"})," de uma constante, e n\xe3o o ",(0,s.jsx)(o.em,{children:"uso"}),"."]})," Declarar e n\xe3o chamar \xe9 o jeito mais\nbarato de uma corre\xe7\xe3o sumir com o quality gate verde."]}),"\n",(0,s.jsxs)(o.p,{children:["O conserto foi cir\xfargico e vale copiar. A AUD1 hoje separa os tr\xeas argumentos do\n",(0,s.jsx)(o.code,{children:"position.set(...)"})," com um ",(0,s.jsx)(o.strong,{children:"varredor de par\xeanteses"})," \u2014 n\xe3o ",(0,s.jsx)(o.code,{children:"split(',')"}),", que cortaria\ndentro da chamada de fun\xe7\xe3o \u2014 e exige ",(0,s.jsx)(o.strong,{children:"nominalmente"})," que o argumento Y chame ",(0,s.jsx)(o.code,{children:"vmOffY("}),".\nE fecha o outro caminho junto (",(0,s.jsx)(o.code,{children:"tools/eval/invariants.mjs:1148-1151"}),"): a f\xf3rmula do\n",(0,s.jsx)(o.code,{children:"vmOffY"})," \xe9 ",(0,s.jsxs)(o.strong,{children:["lida do ",(0,s.jsx)(o.code,{children:"game.js"})," e avaliada"]})," em 16/9, e tem que dar exatamente ",(0,s.jsx)(o.code,{children:"VM_OFF[1]"}),"."]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["Os dois cheques juntos cobrem os dois jeitos de a corre\xe7\xe3o sumir: ",(0,s.jsx)(o.strong,{children:"apagar a CHAMADA"}),"\n(muta\xe7\xe3o medida em 08/2026) ou ",(0,s.jsx)(o.strong,{children:"adulterar a F\xd3RMULA"}),"."]}),"\n"]}),"\n",(0,s.jsx)(o.h3,{id:"n\xe3o-foi-um-caso-isolado--foram-tr\xeas",children:"N\xe3o foi um caso isolado \u2014 foram tr\xeas"}),"\n",(0,s.jsx)(o.p,{children:"O mesmo buraco apareceu em outros dois lugares, e cada um virou uma etapa nova da AUD1:"}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"Muta\xe7\xe3o"}),(0,s.jsx)(o.th,{children:"Placar com a corre\xe7\xe3o desfeita"}),(0,s.jsx)(o.th,{children:"Causa do falso verde"}),(0,s.jsx)(o.th,{children:"Onde"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsxs)(o.td,{children:["Trocar ",(0,s.jsx)(o.code,{children:"vmOffY(...)"})," por ",(0,s.jsx)(o.code,{children:"VM_OFF[1]"})," no argumento Y"]}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.strong,{children:"20/22 verde"})}),(0,s.jsx)(o.td,{children:"a invariante lia a declara\xe7\xe3o, n\xe3o o uso"}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"invariants.mjs:910-920"})})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsxs)(o.td,{children:["Trocar ",(0,s.jsx)(o.code,{children:"g.rotation.set(pit, yaw, t.roll)"})," por ",(0,s.jsx)(o.code,{children:"g.rotation.set(0, 0, t.roll)"})]}),(0,s.jsx)(o.td,{children:"verde"}),(0,s.jsxs)(o.td,{children:["a tabela ",(0,s.jsx)(o.code,{children:"VM_FRAME.cls"})," continua com os \xe2ngulos, e os tr\xeas espelhos continuam batendo ",(0,s.jsx)(o.strong,{children:"entre si"})]}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"invariants.mjs:932-944"})})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsxs)(o.td,{children:["Apagar ",(0,s.jsx)(o.code,{children:"* (weaponCFG(id).vm ?? 1)"})," da escala do mesh"]}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.strong,{children:"28/37 verde, AUD1 inclusive"}),' ("pior \u0394escala 0.0004")']}),(0,s.jsxs)(o.td,{children:["as duas pontas leem ",(0,s.jsx)(o.code,{children:"vm"})," de ",(0,s.jsx)(o.code,{children:"weapons.js"}),"; ",(0,s.jsxs)(o.strong,{children:["o ",(0,s.jsx)(o.code,{children:"game.js"})," nunca \xe9 perguntado"]})," \u2014 era o auditor conferindo a si mesmo"]}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"invariants.mjs:971-975"})})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsxs)(o.td,{children:["Mutar ",(0,s.jsx)(o.code,{children:"this._adsPose['pistol']"})]}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.strong,{children:"20/22 verde"})}),(0,s.jsx)(o.td,{children:"o ADS n\xe3o tinha invariante nenhuma"}),(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"invariants.mjs:1185"})})]})]})]}),"\n",(0,s.jsx)(o.p,{children:"O padr\xe3o comum das quatro \xe9 o mesmo, e \xe9 o que voc\xea deve procurar na sua invariante:"}),"\n",(0,s.jsx)(o.admonition,{title:"O padr\xe3o do falso verde",type:"danger",children:(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.strong,{children:"A r\xe9gua est\xe1 conferindo uma c\xf3pia da regra em vez do jogo."})," Seja porque l\xea a declara\xe7\xe3o\ne n\xe3o o uso, seja porque compara dois espelhos que leem a mesma fonte, seja porque a\ntabela de par\xe2metros continua correta enquanto ningu\xe9m a aplica. Se as duas pontas da sua\ncompara\xe7\xe3o puderem ficar consistentes ",(0,s.jsx)(o.strong,{children:"sem passar pelo c\xf3digo de produ\xe7\xe3o"}),", sua\ninvariante est\xe1 cega."]})}),"\n",(0,s.jsxs)(o.p,{children:[(0,s.jsx)(o.code,{children:"tools/eval/mat-check.mjs:18-27"})," resolve isso da forma mais direta poss\xedvel: o corpo do\n",(0,s.jsx)(o.code,{children:"fixVmMaterials"})," \xe9 ",(0,s.jsxs)(o.strong,{children:["recortado do ",(0,s.jsx)(o.code,{children:"game.js"})," e executado"]})," sobre um material-sonda. Se o\nc\xf3digo mudar, a r\xe9gua muda junto. ",(0,s.jsx)(o.em,{children:'"uma r\xe9gua que carrega uma C\xd3PIA da regra mente no dia\nem que a regra muda."'})]}),"\n",(0,s.jsxs)(o.h3,{id:"muta\xe7\xe3o-como-coisa-de-primeira-classe-ui-checkmjs",children:["Muta\xe7\xe3o como coisa de primeira classe: ",(0,s.jsx)(o.code,{children:"ui-check.mjs"})]}),"\n",(0,s.jsxs)(o.p,{children:["O arn\xeas de UI tem uma ",(0,s.jsx)(o.strong,{children:"tabela de muta\xe7\xf5es versionada"}),", e cada uma declara qual quality gate\ntem que ficar vermelho. ",(0,s.jsx)(o.code,{children:"tools/eval/ui-check.mjs:1046-1050"}),":"]}),"\n",(0,s.jsxs)(o.blockquote,{children:["\n",(0,s.jsxs)(o.p,{children:["Cada muta\xe7\xe3o DESFAZ um dos consertos desta rodada (ou fura um quality gate de prop\xf3sito) e diz\nqual quality gate TEM que ficar vermelho. ",(0,s.jsx)(o.strong,{children:"Uma r\xe9gua que n\xe3o reprova a vers\xe3o anterior do\npr\xf3prio arquivo n\xe3o \xe9 r\xe9gua, \xe9 decora\xe7\xe3o."})]}),"\n"]}),"\n",(0,s.jsx)(o.p,{children:"Rodar uma:"}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-bash",children:"MUT=ui1_ctf_scrim_fraco node tools/eval/ui-check.mjs # espera UI1 VERMELHA\nMUT=ui3_prompt_na_mira node tools/eval/ui-check.mjs # espera UI3 VERMELHA\nMUT=ui2_prompt_eterno node tools/eval/ui-check.mjs # espera UI2 VERMELHA\nMUT=ui4_ctf_sem_relogio node tools/eval/ui-check.mjs # espera UI4 VERMELHA\n"})}),"\n",(0,s.jsxs)(o.p,{children:["As 7 muta\xe7\xf5es est\xe3o em ",(0,s.jsx)(o.code,{children:"tools/eval/ui-check.mjs:1051-1134"}),". Duas mec\xe2nicas: ",(0,s.jsx)(o.code,{children:"css"})," reescreve\no ",(0,s.jsx)(o.code,{children:"public/style.css"})," ",(0,s.jsx)(o.strong,{children:"lido em mem\xf3ria"})," (nunca em disco \u2014 outros agentes est\xe3o editando o\narquivo agora), e ",(0,s.jsx)(o.code,{children:"sim"})," monkey-patcha o objeto ",(0,s.jsx)(o.code,{children:"Game"})," j\xe1 bootado. Se a muta\xe7\xe3o ",(0,s.jsx)(o.code,{children:"css"})," n\xe3o\ncasar com nada, o script sai com c\xf3digo 2 dizendo ",(0,s.jsx)(o.em,{children:'"o CSS mudou de forma"'})," \u2014 porque uma\nmuta\xe7\xe3o que n\xe3o aplica tamb\xe9m \xe9 um falso verde (",(0,s.jsx)(o.code,{children:"ui-check.mjs:1164"}),")."]}),"\n",(0,s.jsx)(o.h2,{id:"como-escrever-uma-invariante",children:"Como escrever uma invariante"}),"\n",(0,s.jsx)(o.p,{children:"Checklist, na ordem:"}),"\n",(0,s.jsxs)(o.ol,{children:["\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Comece pela frase do defeito."})," Literal, com as palavras de quem reclamou. Todo\narn\xeas desta base come\xe7a assim, e n\xe3o \xe9 estilo: \xe9 o que impede a invariante de medir\noutra coisa. Ver o cabe\xe7alho de ",(0,s.jsx)(o.code,{children:"tools/eval/map-check.mjs:5-12"})," \u2014 cinco frases do dono,\ncinco invariantes."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Traduza para uma grandeza mensur\xe1vel."}),' "os jogadores est\xe3o SUBMERSOS EMBAIXO DA\nEST\xc1TUA" \u2192 ',(0,s.jsx)(o.em,{children:"existe geometria vis\xedvel do mapa cujo topo passa de 0,30 m acima do ch\xe3o\nlocal naquele ponto"})," (MAP1). Note que a defini\xe7\xe3o operacional inclui ",(0,s.jsx)(o.strong,{children:"por que 0,30 m"}),':\n\xe9 o degrau que o corpo sobe; acima disso n\xe3o \xe9 "passar por cima", \xe9 "estar dentro".']}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Ache a proced\xeancia do teto."})," Arquivo de refer\xeancia + pixel medido + script que\nreproduz. Se n\xe3o existir, ",(0,s.jsx)(o.strong,{children:"diga que \xe9 fallback"})," e cite a fonte publicada, como o C1\nfaz. Nunca invente o n\xfamero."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Me\xe7a o c\xf3digo de produ\xe7\xe3o, n\xe3o uma c\xf3pia dele."})," Importe o m\xf3dulo real, recorte a\nfun\xe7\xe3o do arquivo e execute, ou exija nominalmente a chamada no texto do fonte."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Mute e confirme que fica vermelha."})," Desfa\xe7a a corre\xe7\xe3o que voc\xea acabou de fazer e\nrode o quality gate. Se ficar verde, sua invariante est\xe1 cega \u2014 volte pro passo 4. Se der pra\nautomatizar, registre a muta\xe7\xe3o numa tabela, como o ",(0,s.jsx)(o.code,{children:"ui-check.mjs"})," faz."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Escreva a evid\xeancia, n\xe3o s\xf3 o booleano."})," O quarto argumento do ",(0,s.jsx)(o.code,{children:"put()"})," \xe9 o que\nalgu\xe9m vai ler daqui a tr\xeas meses: ",(0,s.jsx)(o.code,{children:'"0,504 a 0,619 da altura em 52 medidas | 0 fora da faixa"'})," \xe9 \xfatil; ",(0,s.jsx)(o.code,{children:'"ok"'})," n\xe3o \xe9."]}),"\n",(0,s.jsxs)(o.li,{children:[(0,s.jsx)(o.strong,{children:"Escreva o coment\xe1rio de proced\xeancia acima dela."})," Em portugu\xeas, dizendo o que\naconteceu quando o n\xfamero estava errado. \xc9 esse coment\xe1rio que impede a pr\xf3xima rodada\nde refazer o erro."]}),"\n"]}),"\n",(0,s.jsx)(o.h3,{id:"anti-padr\xf5es-que-j\xe1-custaram-caro-aqui",children:"Anti-padr\xf5es que j\xe1 custaram caro aqui"}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"Anti-padr\xe3o"}),(0,s.jsx)(o.th,{children:"O que deu"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Ler a declara\xe7\xe3o de uma constante em vez do uso"}),(0,s.jsx)(o.td,{children:"20/22 verde com a corre\xe7\xe3o removida"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Dois espelhos que leem a mesma fonte"}),(0,s.jsx)(o.td,{children:'28/37 verde, "pior \u0394escala 0.0004", com o knob desligado'})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Adaptador de formato quebrado em sil\xeancio"}),(0,s.jsxs)(o.td,{children:["VM1\u2013VM6 ficaram ",(0,s.jsx)(o.strong,{children:"PULADAS desde que o auditor existe"})," \u2014 6 invariantes de viewmodel que nunca rodaram uma vez (",(0,s.jsx)(o.code,{children:"invariants.mjs:121-127"}),")"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Medir o v\xe3o contra o ch\xe3o local errado"}),(0,s.jsxs)(o.td,{children:["pickup dentro da piscina reportava v\xe3o ",(0,s.jsx)(o.strong,{children:"0,0000 \u2014 VERDE"})," (",(0,s.jsx)(o.code,{children:"pickup-check.mjs:20-23"}),")"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:'"waypoint \u2264 3 m" como proxy de alcance'}),(0,s.jsxs)(o.td,{children:["74 falsos-positivos e verde em bols\xe3o fechado (",(0,s.jsx)(o.code,{children:"pickup-check.mjs:34-42"}),")"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Piso sem teto"}),(0,s.jsxs)(o.td,{children:['"boca \u2265 0,66" aceita a boca em 0,95 (arma no por\xe3o) \u2014 foi assim que chegamos a 0,816 (',(0,s.jsx)(o.code,{children:"invariants.mjs:432-434"}),")"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Medir num mundo onde o defeito n\xe3o pode acontecer"}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.code,{children:"eval:site"})," cobre ",(0,s.jsx)(o.code,{children:"/ranking"})," e ",(0,s.jsx)(o.strong,{children:"checa corpo"}),", e passou um dia inteiro verde com a p\xe1gina servindo ",(0,s.jsx)(o.strong,{children:"200 com 0 bytes"})," em produ\xe7\xe3o: ele sobe um ",(0,s.jsx)(o.code,{children:"astro dev"})," local, onde ",(0,s.jsx)(o.code,{children:"public/js"})," existe e o ",(0,s.jsx)(o.code,{children:"ENOENT"})," n\xe3o ocorre (BUG-49)"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Aceitar status como prova de p\xe1gina viva"}),(0,s.jsxs)(o.td,{children:["o mesmo BUG-49: ",(0,s.jsx)(o.code,{children:"status === 200"})," chamava de saud\xe1vel uma casca vazia. Corpo agora \xe9 cobrado por ",(0,s.jsx)(o.strong,{children:"tamanho"})]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"N\xfamero medido que ningu\xe9m reprova"}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.code,{children:"gl-metrics.mjs"})," media calls/tri\xe2ngulos desde a rodada 3 e nenhuma cl\xe1usula lia o resultado; o teto s\xf3 existia como prosa num coment\xe1rio. o estacionamento da Loja H (",(0,s.jsx)(o.code,{children:"loja_h"}),") chegou a 4.347 calls antes de algu\xe9m olhar"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"Policiar artefato em vez de fonte"}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.code,{children:"mapa-id-check"})," varria ",(0,s.jsx)(o.code,{children:"public/docs/"})," (sa\xedda do Docusaurus) e ficava vermelha quando o bundle publicado estava uma gera\xe7\xe3o atr\xe1s de um rename \u2014 vermelho sem defeito"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:"R\xe9gua que se acusa"}),(0,s.jsx)(o.td,{children:"a mesma: ela precisa citar os ids antigos para cobr\xe1-los, e se varria a si mesma. Nove ocorr\xeancias, todas dela"})]})]})]}),"\n",(0,s.jsx)(o.h2,{id:"esta-p\xe1gina-\xe9-a-doutrina-o-passo-a-passo-\xe9-uma-skill",children:"Esta p\xe1gina \xe9 a doutrina. O passo a passo \xe9 uma skill"}),"\n",(0,s.jsxs)(o.p,{children:["O que fazer, na ordem, quando algu\xe9m reporta um defeito \u2014 reproduzir, medir antes de\nconsertar, refutar o palpite \xf3bvio, mutar a r\xe9gua, rodar o quality gate na ordem certa e reportar\no que ",(0,s.jsx)(o.strong,{children:"n\xe3o"})," foi verificado \u2014 est\xe1 em ",(0,s.jsx)(o.code,{children:".claude/skills/bug-hunt/SKILL.md"}),", com o caso real\nque comprou cada regra. Ela \xe9 escrita para agente ",(0,s.jsx)(o.strong,{children:"e"})," para gente, e aponta de volta para\nesta p\xe1gina em vez de repeti-la."]}),"\n",(0,s.jsxs)(o.h2,{id:"port\xf5es-que-n\xe3o-cabem-no-check-e-por-qu\xea",children:["Port\xf5es que N\xc3O cabem no ",(0,s.jsx)(o.code,{children:"check"}),", e por qu\xea"]}),"\n",(0,s.jsxs)(o.p,{children:["Tr\xeas r\xe9guas exigem insumo que o port\xe3o r\xe1pido n\xe3o tem \u2014 navegador, ou o build pronto. Elas\nficam de fora de prop\xf3sito e s\xe3o passo de pr\xe9-deploy, junto do ",(0,s.jsx)(o.code,{children:"eval:boot"}),". Cada uma nasceu\nde um defeito que os port\xf5es existentes n\xe3o podiam ver:"]}),"\n",(0,s.jsxs)(o.table,{children:[(0,s.jsx)(o.thead,{children:(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.th,{children:"Comando"}),(0,s.jsx)(o.th,{children:"O que mede"}),(0,s.jsx)(o.th,{children:"O buraco que fechou"})]})}),(0,s.jsxs)(o.tbody,{children:[(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"npm run eval:ssr"})}),(0,s.jsxs)(o.td,{children:["toda p\xe1gina ",(0,s.jsx)(o.code,{children:"prerender = false"})," entrega ",(0,s.jsx)(o.strong,{children:"corpo"}),", medido no artefato do build, entrando no diret\xf3rio da fun\xe7\xe3o (o cwd de produ\xe7\xe3o)"]}),(0,s.jsxs)(o.td,{children:[(0,s.jsx)(o.code,{children:"/mapa"}),", ",(0,s.jsx)(o.code,{children:"/ranking"})," e ",(0,s.jsx)(o.code,{children:"/u/*"})," serviram ",(0,s.jsx)(o.strong,{children:"200 com 0 bytes"})," por um dia. O ",(0,s.jsx)(o.code,{children:"eval:site"})," mede um ",(0,s.jsx)(o.code,{children:"astro dev"})," local, onde o defeito n\xe3o pode acontecer"]})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"npm run eval:cena"})}),(0,s.jsxs)(o.td,{children:["teto de draw calls e tri\xe2ngulos por frame, ",(0,s.jsx)(o.strong,{children:"por mapa"})]}),(0,s.jsx)(o.td,{children:"o n\xfamero era medido desde a rodada 3 e ningu\xe9m reprovava. Descobriu que o mapa da Quebrada custa 1.8 k calls e roda a metade do fps dos outros"})]}),(0,s.jsxs)(o.tr,{children:[(0,s.jsx)(o.td,{children:(0,s.jsx)(o.code,{children:"npm run eval:mapid"})}),(0,s.jsx)(o.td,{children:"nenhum id no estilo Counter-Strike sobrevive, todo id antigo resolve, e toda pr\xe9via existe em disco"}),(0,s.jsxs)(o.td,{children:["renomear id sem renomear a imagem deixa cartaz quebrado no menu: 404 no navegador, ",(0,s.jsx)(o.strong,{children:"nada"})," no build"]})]})]})]}),"\n",(0,s.jsxs)(o.p,{children:["O teto do ",(0,s.jsx)(o.code,{children:"eval:cena"})," mora em ",(0,s.jsx)(o.code,{children:"tools/eval/cena-tetos.mjs"}),", importado tanto pela r\xe9gua de\nnavegador quanto pelas cl\xe1usulas ",(0,s.jsx)(o.code,{children:"CENA"})," do ",(0,s.jsx)(o.code,{children:"invariants.mjs"})," \u2014 um limiar, dois leitores. Dois\nn\xfameros para o mesmo conceito \xe9 o instrumento discordando de si, e isso j\xe1 custou uma rodada\ninteira aqui."]}),"\n",(0,s.jsx)(o.h2,{id:"rodar-o-quality-gate",children:"Rodar o quality gate"}),"\n",(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-bash",children:"node tools/eval/invariants.mjs # tudo que roda sem browser\nnode tools/eval/invariants.mjs --json # sa\xedda pra m\xe1quina\nnpm run check # syntax + quality gate + vm + coice + bots\nnpm run check:fast # segundos \u2014 rode este primeiro, sempre\n\n# pr\xe9-deploy: exigem navegador ou build, e por isso ficam fora dos de cima\nnpm run eval:boot # o jogo ABRE?\nnpm run build && npm run eval:ssr # p\xe1gina SSR entrega corpo?\nnpm run eval:cena # custo de cena dentro do teto?\n"})}),"\n",(0,s.jsxs)(o.p,{children:["Fontes atuais de produ\xe7\xe3o, dados e d\xedvida conhecida: ",(0,s.jsx)(o.a,{href:"/docs/estado",children:"Estado atual"}),"."]})]})}function m(e={}){const{wrapper:o}={...(0,n.R)(),...e.components};return o?(0,s.jsx)(o,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},8453(e,o,a){a.d(o,{R:()=>i,x:()=>d});var r=a(6540);const s={},n=r.createContext(s);function i(e){const o=r.useContext(n);return r.useMemo(function(){return"function"==typeof e?e(o):{...o,...e}},[o,e])}function d(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),r.createElement(n.Provider,{value:o},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/assets/js/runtime~main.c8068ae7.js b/public/docs/assets/js/runtime~main.30b9a464.js similarity index 84% rename from public/docs/assets/js/runtime~main.c8068ae7.js rename to public/docs/assets/js/runtime~main.30b9a464.js index 6ea7018f2..0e90a3acf 100644 --- a/public/docs/assets/js/runtime~main.c8068ae7.js +++ b/public/docs/assets/js/runtime~main.30b9a464.js @@ -1 +1 @@ -(()=>{"use strict";var e={};const t={};function r(o){const n=t[o];if(void 0!==n)return n.exports;const a=t[o]={exports:{}};return e[o].call(a.exports,a,a.exports,r),a.exports}r.m=e,(()=>{const e=[];r.O=(t,o,n,a)=>{if(o){a=a||0;for(var c=e.length;c>0&&e[c-1][2]>a;c--)e[c]=e[c-1];return void(e[c]=[o,n,a])}let s=1/0;for(c=0;c=a)&&Object.keys(r.O).every(e=>r.O[e](o[i]))?o.splice(i--,1):(l=!1,a{const t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},(()=>{const e=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;let t;r.t=function(o,n){if(1&n&&(o=this(o)),8&n)return o;if("object"==typeof o&&o){if(4&n&&o.__esModule)return o;if(16&n&&"function"==typeof o.then)return o}const a=Object.create(null);r.r(a);const c={};t=t||[null,e({}),e([]),e(e)];for(var s=2&n&&o;("object"==typeof s||"function"==typeof s)&&!~t.indexOf(s);s=e(s))Object.getOwnPropertyNames(s).forEach(e=>c[e]=()=>o[e]);return c.default=()=>o,r.d(a,c),a}})(),r.d=(e,t)=>{if(Array.isArray(t))for(var o=0;oPromise.all(Object.keys(r.f).reduce((t,o)=>(r.f[o](e,t),t),[])),r.u=e=>"assets/js/"+({48:"a94703ab",98:"a7bd4aaa",256:"11b43341",257:"f641f320",401:"17896441",475:"e8699d1d",514:"393ca303",568:"1be81749",647:"5e95c892",659:"7172ebb0",742:"aba21aa0",796:"44959d42",818:"023c5ebe",889:"745ddde7"}[e]||e)+"."+{48:"c13f04ba",98:"2eacc047",237:"7f505004",256:"3a34a08e",257:"6ca27443",401:"02cdecdc",475:"1e1639d2",514:"bca993db",568:"cc5d4388",647:"00e4eac4",659:"7e9aa7da",742:"a2338f79",796:"590f9b1e",818:"c5b0f6ca",889:"cbd3d5a7"}[e]+".js",r.miniCssF=e=>{},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{const e={},t="coro-solto-docs:";r.l=(o,n,a,c)=>{if(e[o])return void e[o].push(n);let s,i;if(void 0!==a){const e=document.getElementsByTagName("script");for(var l=0;l{s.onerror=s.onload=null,clearTimeout(d);const n=e[o];if(delete e[o],s.parentNode?.removeChild(s),n?.forEach(e=>e(r)),t)return t(r)},d=setTimeout(f.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=f.bind(null,s.onerror),s.onload=f.bind(null,s.onload),i&&document.head.appendChild(s)}})(),r.r=e=>{Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.dn=e=>{var t=Object.getOwnPropertyDescriptor(e,"name");(!t||!t.writable&&t.configurable)&&Object.defineProperty(e,"name",{value:"default",configurable:!0})},r.cjs=e=>{const t={exports:{}};return e.call(t.exports,t,t.exports),t.exports},r.p="/docs/",r.gca=function(e){return e={17896441:"401",a94703ab:"48",a7bd4aaa:"98","11b43341":"256",f641f320:"257",e8699d1d:"475","393ca303":"514","1be81749":"568","5e95c892":"647","7172ebb0":"659",aba21aa0:"742","44959d42":"796","023c5ebe":"818","745ddde7":"889"}[e]||e,r.p+r.u(e)},(()=>{const e={354:0,869:0};r.f.j=(t,o)=>{let n=r.o(e,t)?e[t]:void 0;if(0!==n)if(n)o.push(n[2]);else if(/^(354|869)$/.test(t))e[t]=0;else{const a=new Promise((r,o)=>n=e[t]=[r,o]);o.push(n[2]=a);const c=r.p+r.u(t),s=new Error,i=o=>{if(r.o(e,t)&&(n=e[t],0!==n&&(e[t]=void 0),n)){const e=o&&("load"===o.type?"missing":o.type),r=o&&o.target&&o.target.src;s.message="Loading chunk "+t+" failed.\n("+e+": "+r+")",s.name="ChunkLoadError",s.type=e,s.request=r,s.event=o,n[1](s)}};r.l(c,i,"chunk-"+t,t)}},r.O.j=t=>0===e[t];const t=(t,o)=>{let[n,a,c]=o;var s,i,l=0;if(n.some(t=>0!==e[t])){for(s in a)r.o(a,s)&&(r.m[s]=a[s]);if(c)var f=c(r)}for(t&&t(o);l{"use strict";var e={};const t={};function r(o){const n=t[o];if(void 0!==n)return n.exports;const a=t[o]={exports:{}};return e[o].call(a.exports,a,a.exports,r),a.exports}r.m=e,(()=>{const e=[];r.O=(t,o,n,a)=>{if(o){a=a||0;for(var c=e.length;c>0&&e[c-1][2]>a;c--)e[c]=e[c-1];return void(e[c]=[o,n,a])}let s=1/0;for(c=0;c=a)&&Object.keys(r.O).every(e=>r.O[e](o[i]))?o.splice(i--,1):(l=!1,a{const t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},(()=>{const e=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;let t;r.t=function(o,n){if(1&n&&(o=this(o)),8&n)return o;if("object"==typeof o&&o){if(4&n&&o.__esModule)return o;if(16&n&&"function"==typeof o.then)return o}const a=Object.create(null);r.r(a);const c={};t=t||[null,e({}),e([]),e(e)];for(var s=2&n&&o;("object"==typeof s||"function"==typeof s)&&!~t.indexOf(s);s=e(s))Object.getOwnPropertyNames(s).forEach(e=>c[e]=()=>o[e]);return c.default=()=>o,r.d(a,c),a}})(),r.d=(e,t)=>{if(Array.isArray(t))for(var o=0;oPromise.all(Object.keys(r.f).reduce((t,o)=>(r.f[o](e,t),t),[])),r.u=e=>"assets/js/"+({48:"a94703ab",98:"a7bd4aaa",256:"11b43341",257:"f641f320",401:"17896441",475:"e8699d1d",514:"393ca303",568:"1be81749",647:"5e95c892",659:"7172ebb0",742:"aba21aa0",796:"44959d42",818:"023c5ebe",889:"745ddde7"}[e]||e)+"."+{48:"c13f04ba",98:"2eacc047",237:"7f505004",256:"3a34a08e",257:"6ca27443",401:"02cdecdc",475:"ac593cb4",514:"501859ff",568:"06cc6d24",647:"00e4eac4",659:"7e9aa7da",742:"a2338f79",796:"ed546c78",818:"c5b0f6ca",889:"0263fb18"}[e]+".js",r.miniCssF=e=>{},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{const e={},t="coro-solto-docs:";r.l=(o,n,a,c)=>{if(e[o])return void e[o].push(n);let s,i;if(void 0!==a){const e=document.getElementsByTagName("script");for(var l=0;l{s.onerror=s.onload=null,clearTimeout(f);const n=e[o];if(delete e[o],s.parentNode?.removeChild(s),n?.forEach(e=>e(r)),t)return t(r)},f=setTimeout(d.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=d.bind(null,s.onerror),s.onload=d.bind(null,s.onload),i&&document.head.appendChild(s)}})(),r.r=e=>{Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.dn=e=>{var t=Object.getOwnPropertyDescriptor(e,"name");(!t||!t.writable&&t.configurable)&&Object.defineProperty(e,"name",{value:"default",configurable:!0})},r.cjs=e=>{const t={exports:{}};return e.call(t.exports,t,t.exports),t.exports},r.p="/docs/",r.gca=function(e){return e={17896441:"401",a94703ab:"48",a7bd4aaa:"98","11b43341":"256",f641f320:"257",e8699d1d:"475","393ca303":"514","1be81749":"568","5e95c892":"647","7172ebb0":"659",aba21aa0:"742","44959d42":"796","023c5ebe":"818","745ddde7":"889"}[e]||e,r.p+r.u(e)},(()=>{const e={354:0,869:0};r.f.j=(t,o)=>{let n=r.o(e,t)?e[t]:void 0;if(0!==n)if(n)o.push(n[2]);else if(/^(354|869)$/.test(t))e[t]=0;else{const a=new Promise((r,o)=>n=e[t]=[r,o]);o.push(n[2]=a);const c=r.p+r.u(t),s=new Error,i=o=>{if(r.o(e,t)&&(n=e[t],0!==n&&(e[t]=void 0),n)){const e=o&&("load"===o.type?"missing":o.type),r=o&&o.target&&o.target.src;s.message="Loading chunk "+t+" failed.\n("+e+": "+r+")",s.name="ChunkLoadError",s.type=e,s.request=r,s.event=o,n[1](s)}};r.l(c,i,"chunk-"+t,t)}},r.O.j=t=>0===e[t];const t=(t,o)=>{let[n,a,c]=o;var s,i,l=0;if(n.some(t=>0!==e[t])){for(s in a)r.o(a,s)&&(r.m[s]=a[s]);if(c)var d=c(r)}for(t&&t(o);l BotBrain | CORO SOLTO — Docs do Dev - + diff --git a/public/docs/colaborar/index.html b/public/docs/colaborar/index.html index cd601ab94..89b6102e7 100644 --- a/public/docs/colaborar/index.html +++ b/public/docs/colaborar/index.html @@ -4,7 +4,7 @@ Como colaborar | CORO SOLTO — Docs do Dev - + @@ -12,7 +12,7 @@

O número abaixo não é retórica, e não é escrito à mão: sai de git shortlog -sn --no-merges descontando os autores que são agentes de IA (que assinam como Claude / Claude (gauntlet …)).

-

11 identidades de autoria humana assinam commit no histórico desta branch: ruben-cytonic, Emerson Garrido, Ruben, rubenmarcus, Ruben Marcus, William Oliveira, Juan Versolato Lopes, daeeseD, matheusgb, Maná Soares, daltonfontes. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém não aparece aqui.

+

11 identidades de autoria humana assinam commit no histórico desta branch: ruben-cytonic, Ruben, Emerson Garrido, rubenmarcus, Ruben Marcus, William Oliveira, Juan Versolato Lopes, daeeseD, matheusgb, Maná Soares, daltonfontes. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém não aparece aqui.

Bloco gerado por node tools/gen-docs.mjs. Fonte: git shortlog -sn --no-merges (descontando autores que são agentes)

diff --git a/public/docs/en/404.html b/public/docs/en/404.html index 9eab0b53a..ca52eba08 100644 --- a/public/docs/en/404.html +++ b/public/docs/en/404.html @@ -4,7 +4,7 @@ CORO SOLTO — Docs do Dev - + diff --git a/public/docs/en/ai-instrumentation/index.html b/public/docs/en/ai-instrumentation/index.html index 319c8102f..38036a20c 100644 --- a/public/docs/en/ai-instrumentation/index.html +++ b/public/docs/en/ai-instrumentation/index.html @@ -4,7 +4,7 @@ AI instrumentation: how the work gets done | CORO SOLTO — Docs do Dev - + diff --git a/public/docs/en/architecture/index.html b/public/docs/en/architecture/index.html index 6ab248caa..bf409c7ac 100644 --- a/public/docs/en/architecture/index.html +++ b/public/docs/en/architecture/index.html @@ -4,7 +4,7 @@ Architecture: N agents in the same file | CORO SOLTO — Docs do Dev - + @@ -41,8 +41,8 @@

The indexed fi

Size of the files gen-arch.mjs indexes — a generated block, regenerated by npm run docs and checked by npm run docs:check:

-
FileLines
public/js/game.js6,838
public/js/main.js2,646
public/js/characters.js1,068
public/js/glbchars.js837
public/js/vmattach.js628
public/js/weapons.js344
public/js/springs.js260
-

Total in public/js/: 31,744 lines in 44 files. The symbol-to-line index lives in tools/eval/ARCH.md.

+
FileLines
public/js/game.js6,910
public/js/main.js2,698
public/js/characters.js1,068
public/js/glbchars.js844
public/js/vmattach.js628
public/js/weapons.js346
public/js/springs.js260
+

Total in public/js/: 32,001 lines in 44 files. The symbol-to-line index lives in tools/eval/ARCH.md.

Block generated by node tools/gen-docs.mjs. Source: wc -l public/js/*.js

diff --git a/public/docs/en/assets/js/7cf461fa.6b3dd629.js b/public/docs/en/assets/js/7cf461fa.1cb4b3cb.js similarity index 98% rename from public/docs/en/assets/js/7cf461fa.6b3dd629.js rename to public/docs/en/assets/js/7cf461fa.1cb4b3cb.js index 1277ae0ea..c75d7d361 100644 --- a/public/docs/en/assets/js/7cf461fa.6b3dd629.js +++ b/public/docs/en/assets/js/7cf461fa.1cb4b3cb.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[3],{5805(e,n,s){s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>a,frontMatter:()=>o,metadata:()=>t,toc:()=>h});const t=JSON.parse('{"id":"arquitetura","title":"Architecture: N agents in the same file","description":"The real architecture, generated by tools/gen-arch.mjs \u2014 and the disjoint line-range mechanism that lets agents work in parallel without collision.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md","sourceDirName":".","slug":"/architecture","permalink":"/docs/en/architecture","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/arquitetura.md","tags":[],"version":"current","sidebarPosition":5,"frontMatter":{"id":"arquitetura","title":"Architecture: N agents in the same file","sidebar_label":"Architecture","sidebar_position":5,"slug":"/architecture","description":"The real architecture, generated by tools/gen-arch.mjs \u2014 and the disjoint line-range mechanism that lets agents work in parallel without collision."},"sidebar":"dev","previous":{"title":"BotBrain","permalink":"/docs/en/botbrain"},"next":{"title":"How to contribute","permalink":"/docs/en/contributing"}}');var i=s(4848),r=s(8453);const o={id:"arquitetura",title:"Architecture: N agents in the same file",sidebar_label:"Architecture",sidebar_position:5,slug:"/architecture",description:"The real architecture, generated by tools/gen-arch.mjs \u2014 and the disjoint line-range mechanism that lets agents work in parallel without collision."},d="Architecture: N agents in the same file",c={},h=[{value:"Why this document is generated by a script",id:"why-generated-by-script",level:2},{value:"The indexed files",id:"indexed-files",level:2},{value:"The largest methods in game.js \u2014 where the conflict lives",id:"largest-methods",level:3},{value:"Disjoint line ranges",id:"disjoint-line-ranges",level:2},{value:"How it works",id:"how-it-works",level:3},{value:"The conflict table",id:"conflict-table",level:3},{value:"The red zones",id:"red-zones",level:3},{value:"The operating rules",id:"operating-rules",level:3},{value:"The three zones of the repository",id:"three-zones",level:2},{value:"Practical consequence",id:"practical-consequence",level:3},{value:"Content data system",id:"content-data-system",level:2},{value:"What is generated, and what is not",id:"generated-vs-not",level:2},{value:"What the generator does NOT solve: arquivo:linha pointers in prose",id:"what-the-generator-does-not-solve",level:3}];function l(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:["\n",(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"architecture-n-agents-in-the-same-file",children:"Architecture: N agents in the same file"})}),"\n",(0,i.jsx)(n.h2,{id:"why-generated-by-script",children:"Why this document is generated by a script"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"tools/eval/ARCH.md"})," is not hand-written. It is generated by ",(0,i.jsx)(n.code,{children:"node tools/gen-arch.mjs"}),",\nand the reason is in the script's header (",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:5-8"}),"):"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["The hand-written ",(0,i.jsx)(n.code,{children:"ARCH.md"}),' said "game.js (3234 lines)" when the file had 5361.\nEvery ',(0,i.jsx)(n.code,{children:"arquivo:linha"})," pointer in the conflict table was off \u2014 and that\ntable is precisely what keeps two agents (or two contributors) from editing the\nsame region. A hand-written line-number index goes stale on the first\ncommit; the only fix is to generate it."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["And the separation that makes this work (",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:11-13"}),", quoted verbatim \u2014\nthe source comments are in Portuguese):"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"frente -> S\xcdMBOLO = conhecimento humano, est\xe1vel, vive nas FRENTES do script\ns\xedmbolo -> LINHA = vol\xe1til, \xe9 o que este script resolve toda vez\n"})}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:'Translation: "front \u2192 SYMBOL = human knowledge, stable, lives in the script\'s FRENTES;\nsymbol \u2192 LINE = volatile, it is what this script resolves every time."'})}),"\n",(0,i.jsxs)(n.p,{children:["The old ",(0,i.jsx)(n.code,{children:"ARCH.md"})," pinned ",(0,i.jsx)(n.strong,{children:"front \u2192 line"}),", mixing the two shelf lives. It is\na small idea with a big consequence: the work partition is declared in terms\nthat do not change (method names), and the resolution to volatile coordinates (line\nnumbers) is recomputed on every run."]}),"\n",(0,i.jsxs)(n.admonition,{type:"note",children:[(0,i.jsxs)(n.mdxAdmonitionTitle,{children:["The ",(0,i.jsx)(n.code,{children:"arch:check"})," is RED right now \u2014 and that is the best demonstration on this page"]}),(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"npm run arch"})," and ",(0,i.jsx)(n.code,{children:"npm run arch:check"})," exist today in the root ",(0,i.jsx)(n.code,{children:"package.json"}),", and the check\nis not passing. Gate output below, quoted verbatim \u2014 the tools print in Portuguese:"]}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"$ npm run arch:check\n\u2717 ARCH1 ARCH.md est\xe1 DESATUALIZADO em rela\xe7\xe3o ao c\xf3digo.\n game.js tem 6428 linhas; o \xedndice do ARCH.md n\xe3o bate.\n Rode: npm run arch\n"})}),(0,i.jsxs)(n.p,{children:["The message misleads on purpose: it ",(0,i.jsx)(n.strong,{children:"talks about lines because that is the summary it\nknows how to print"}),", but what ",(0,i.jsx)(n.code,{children:"--check"})," compares is the entire generated block, byte by byte \u2014 and that\nblock also carries the game's version number. Correct symbol index plus stale version gives\nthe same red. One command fixes it."]}),(0,i.jsxs)(n.p,{children:["A caveat that still holds: in CI the step has ",(0,i.jsx)(n.code,{children:"continue-on-error: true"}),", so the\ncheck runs but ",(0,i.jsx)(n.strong,{children:"does not block"})," \u2014 which is exactly how it managed to stay\nred without anyone noticing. Removing that line is what turns it into a real\ngate."]})]}),"\n",(0,i.jsx)(n.h2,{id:"indexed-files",children:"The indexed files"}),"\n",(0,i.jsxs)(n.p,{children:["Size of the files ",(0,i.jsx)(n.code,{children:"gen-arch.mjs"})," indexes \u2014 a generated block, regenerated by\n",(0,i.jsx)(n.code,{children:"npm run docs"})," and checked by ",(0,i.jsx)(n.code,{children:"npm run docs:check"}),":"]}),"\n","\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"File"}),(0,i.jsx)(n.th,{style:{textAlign:"right"},children:"Lines"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/game.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"6,838"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/main.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"2,646"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/characters.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"1,068"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/glbchars.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"837"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/vmattach.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"628"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/weapons.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"344"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/springs.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"260"})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:["Total in ",(0,i.jsx)(n.code,{children:"public/js/"}),": ",(0,i.jsx)(n.strong,{children:"31,744 lines in 44 files"}),". The symbol-to-line index lives in ",(0,i.jsx)(n.code,{children:"tools/eval/ARCH.md"}),"."]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Block generated by ",(0,i.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,i.jsx)(n.code,{children:"wc -l public/js/*.js"})]}),"\n"]}),"\n","\n",(0,i.jsxs)(n.h3,{id:"largest-methods",children:["The largest methods in ",(0,i.jsx)(n.code,{children:"game.js"})," \u2014 where the conflict lives"]}),"\n",(0,i.jsxs)(n.p,{children:["This table is ",(0,i.jsx)(n.strong,{children:"not reproduced here"}),", and the reason is this page's own thesis: it is\n",(0,i.jsx)(n.code,{children:"linha \u2192 m\xe9todo"}),", the volatile side of the separation, and duplicating it in a prose page creates a\nsecond copy that ages on its own. It lives generated, in one place only:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"npm run arch # regenerates tools/eval/ARCH.md\nnode tools/gen-arch.mjs --json # the raw index, for another tool\n"})}),"\n",(0,i.jsxs)(n.p,{children:["What does ",(0,i.jsx)(n.strong,{children:"not"})," age, and is therefore written here: ",(0,i.jsx)(n.code,{children:"_updateBot()"})," is by far the largest\nmethod in the file and is flagged by the index itself as an ",(0,i.jsx)(n.strong,{children:"extraction candidate"}),";\n",(0,i.jsx)(n.code,{children:"constructor()"}),", ",(0,i.jsx)(n.code,{children:"update()"})," and ",(0,i.jsx)(n.code,{children:"_dom()"})," are ",(0,i.jsx)(n.strong,{children:"red zone, append-only"}),", because any\nfront may need them. Big method = unreviewable PR and conflicting merge \u2014 extracting\n",(0,i.jsx)(n.code,{children:"_updateBot"})," is high-value, medium-risk work, and it requires coordinating first, because the\nregion is contested."]}),"\n",(0,i.jsx)(n.h2,{id:"disjoint-line-ranges",children:"Disjoint line ranges"}),"\n",(0,i.jsxs)(n.p,{children:["This is the mechanism that lets several agents (or contributors) edit the ",(0,i.jsx)(n.strong,{children:"same\nfile"})," \u2014 the largest in the repository, thousands of lines \u2014 at the same time, without merge\nconflict."]}),"\n",(0,i.jsx)(n.h3,{id:"how-it-works",children:"How it works"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Each front declares SYMBOLS, never lines."})," In ",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:32-73"}),", the\n",(0,i.jsx)(n.code,{children:"FRENTES"})," constant lists, per front, three things: exclusive ",(0,i.jsx)(n.code,{children:"arquivos"}),", ",(0,i.jsx)(n.code,{children:"simbolos"}),"\n(methods) and ",(0,i.jsx)(n.code,{children:"consts"})," (top-level constants). For example, the ARMAS/VIEWMODEL front owns\n",(0,i.jsx)(n.code,{children:"_buildViewModels"}),", ",(0,i.jsx)(n.code,{children:"_vmFrame"}),", ",(0,i.jsx)(n.code,{children:"_tryShoot"}),", ",(0,i.jsx)(n.code,{children:"_shotRecoil"}),"\u2026 and the constants ",(0,i.jsx)(n.code,{children:"WEAPONS"}),",\n",(0,i.jsx)(n.code,{children:"VM_FOV_DEFAULT"}),", ",(0,i.jsx)(n.code,{children:"VM_OFF"}),", ",(0,i.jsx)(n.code,{children:"REC_DEG"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"The script indexes the file and resolves symbol \u2192 range."})," ",(0,i.jsx)(n.code,{children:"indexar()"}),"\n(",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:80-111"}),") scans the file line by line with three patterns: class\nmethod (exactly 2 spaces of indentation), ",(0,i.jsx)(n.strong,{children:"arrow method assigned at runtime"}),"\n(",(0,i.jsx)(n.code,{children:"this._vmFrame = (force) => {"}),") and top-level declaration. Each symbol ends where the\nnext one begins."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Contiguous ranges are merged"})," (gap \u2264 12 lines) to keep the table readable\n(",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:172-178"}),")."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Overlap between fronts is detected"}),", because a conflict table that contradicts\nitself is worse than none (",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:190-200"}),")."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["The detail in step 2 deserves highlighting: v1 of the script only saw class methods, so\n",(0,i.jsx)(n.code,{children:"_vmFrame"})," \u2014 about 100 lines born ",(0,i.jsx)(n.strong,{children:"inside"})," another method, as an arrow that\ncloses over local variables \u2014 was ",(0,i.jsx)(n.strong,{children:"invisible in the index"}),"\n(",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:95-97"}),"). An index that cannot see the most contested method in the file is\nworse than no index at all, because it gives false confidence."]}),"\n",(0,i.jsx)(n.h3,{id:"conflict-table",children:"The conflict table"}),"\n",(0,i.jsxs)(n.p,{children:["From ",(0,i.jsx)(n.code,{children:"tools/eval/ARCH.md"})," (generated block \u2014 the ranges below are from the previous generation; run\n",(0,i.jsx)(n.code,{children:"node tools/gen-arch.mjs"})," for today's):"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Front"}),(0,i.jsx)(n.th,{children:"Exclusive files"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"ARMAS / VIEWMODEL"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"vmattach.js"})," ",(0,i.jsx)(n.code,{children:"springs.js"})," ",(0,i.jsx)(n.code,{children:"weapons.js"})," ",(0,i.jsx)(n.code,{children:"fparms.js"})," ",(0,i.jsx)(n.code,{children:"handik.js"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"BOTS / JOGABILIDADE"})}),(0,i.jsxs)(n.td,{children:["\u2014 (ranges in ",(0,i.jsx)(n.code,{children:"game.js"})," only)"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"MAPAS / MUNDO"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"maps.js"})," ",(0,i.jsx)(n.code,{children:"mapprops.js"})," ",(0,i.jsx)(n.code,{children:"map_brasilia.js"})," ",(0,i.jsx)(n.code,{children:"map_havan.js"})," ",(0,i.jsx)(n.code,{children:"map_piscina.js"})," ",(0,i.jsx)(n.code,{children:"map_piscinao_ramos.js"})," ",(0,i.jsx)(n.code,{children:"map_ferrovelho.js"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"GR\xc1FICOS / FX"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"bloom.js"})," ",(0,i.jsx)(n.code,{children:"textures.js"})," ",(0,i.jsx)(n.code,{children:"vao.js"})," ",(0,i.jsx)(n.code,{children:"stylize.js"})," ",(0,i.jsx)(n.code,{children:"gpuparticles.js"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"UI / HUD / MENU"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"main.js"})," ",(0,i.jsx)(n.code,{children:"public/style.css"})," ",(0,i.jsx)(n.code,{children:"src/pages/index.astro"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"\xc1UDIO"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"audio.js"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"PERSONAGENS"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"characters.js"})," ",(0,i.jsx)(n.code,{children:"glbchars.js"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"SITE / BACKEND"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"src/"})})]})]})]}),"\n",(0,i.jsxs)(n.admonition,{title:"Two map files have NO declared owner",type:"caution",children:[(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"map_quebrada.js"})," (1.319 lines, the newest map) and ",(0,i.jsx)(n.code,{children:"map_decals.js"})," ",(0,i.jsx)(n.strong,{children:"appear in no\nfront at all"})," in ",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs"})," \u2014 the list above is a faithful copy of ",(0,i.jsx)(n.code,{children:"FRENTES"}),", and they\nare not there. Whoever edits those two collides with nobody ",(0,i.jsx)(n.em,{children:"according to the table"}),", which is\nprecisely the guarantee the table is supposed to give and does not. Adding them is one line in\n",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs"})," followed by ",(0,i.jsx)(n.code,{children:"npm run arch"}),"."]}),(0,i.jsxs)(n.p,{children:["(",(0,i.jsx)(n.code,{children:"map.js"})," was once listed here and ",(0,i.jsx)(n.strong,{children:"no longer exists"}),': it was the "Pra\xe7a (cl\xe1ssico)", deleted\nalong with the ',(0,i.jsx)(n.code,{children:"praca_old"})," map.)"]})]}),"\n",(0,i.jsx)(n.h3,{id:"red-zones",children:"The red zones"}),"\n",(0,i.jsxs)(n.p,{children:["Three methods are ",(0,i.jsx)(n.strong,{children:"append-only"}),", because any front may need them\n(",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:75-77"}),"):"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"update()"})," \u2014 the loop"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"_dom()"})," \u2014 the HUD wiring"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"constructor()"})," \u2014 one of the largest methods in the file (today's size is in ",(0,i.jsx)(n.code,{children:"ARCH.md"}),")"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Editing the middle of these is the fastest way for two contributors to trample each other.\nAppend at the end; do not reorganize."}),"\n",(0,i.jsx)(n.h3,{id:"operating-rules",children:"The operating rules"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Declare your front before editing."})," If it is a human PR, say so in the description."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsxs)(n.strong,{children:["In ",(0,i.jsx)(n.code,{children:"game.js"}),", edit by chunk \u2014 never overwrite the whole file."]})," A\ntool that rewrites the file erases the work of whoever is on the other range."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Two fronts with disjoint ranges run in parallel."})," The generated ",(0,i.jsx)(n.code,{children:"ARCH.md"})," records\nthat this was measured: ",(0,i.jsx)(n.em,{children:'"3 agents edited disjoint ranges simultaneously with zero\ncontent conflict"'})," (",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:163"}),")."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Touched a symbol? Move the name in the front's declaration, not the number."})," The script\nwarns when a declared symbol disappears from the code."]}),"\n"]}),"\n",(0,i.jsx)(n.admonition,{title:"Why this matters to you, human",type:"tip",children:(0,i.jsxs)(n.p,{children:["The same partition that prevents collisions between agents is what makes a PR of yours reviewable. A\nPR that touches ",(0,i.jsx)(n.code,{children:"_updateBot"})," + ",(0,i.jsx)(n.code,{children:"style.css"})," + ",(0,i.jsx)(n.code,{children:"map_havan.js"})," is three PRs hidden in one, and\nwill collide with three different fronts. One PR per front lands fast."]})}),"\n",(0,i.jsx)(n.h2,{id:"three-zones",children:"The three zones of the repository"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"public/ game vanilla ES modules, zero build, vendored Three.js\nsrc/ site Astro + Vercel adapter, SSR API routes\ntools/ harness .mjs/.py scripts \u2014 the ruler, the gate and the probes\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Versions, counts and what each tool does are in\n",(0,i.jsx)(n.a,{href:"/docs/en/stack",children:"Stack and tools"})," \u2014 ",(0,i.jsx)(n.strong,{children:"generated"}),", not hand-written."]}),"\n",(0,i.jsx)(n.p,{children:"The coupling between them is deliberately thin and worth understanding:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"The site loads the game via import map"}),", in ",(0,i.jsx)(n.code,{children:"src/pages/index.astro:97-123"}),". It is the only\nplace where Astro knows the game's modules exist."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"The harness loads the game straight from disk"}),", with no browser: ",(0,i.jsx)(n.code,{children:"tools/eval/harness.mjs"})," stubs\nDOM/canvas/",(0,i.jsx)(n.code,{children:"fetch"})," and imports ",(0,i.jsx)(n.code,{children:"public/js/game.js"})," as a module. That is why the gate measures\nproduction code, not a reimplementation."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:(0,i.jsx)(n.code,{children:"tools/eval/serve.mjs:15"})})," bridges to the test case: it serves ",(0,i.jsx)(n.code,{children:"public/"})," and maps\n",(0,i.jsx)(n.code,{children:"/"})," to the ",(0,i.jsx)(n.code,{children:"index.astro"})," source, with no Astro in the path."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"practical-consequence",children:"Practical consequence"}),"\n",(0,i.jsxs)(n.p,{children:["The game ",(0,i.jsx)(n.strong,{children:"cannot"})," gain a runtime dependency or a build step. This is not\nconservatism: it is what lets ",(0,i.jsx)(n.code,{children:"harness.mjs"})," boot the ",(0,i.jsx)(n.code,{children:"Game"})," class in pure node\nin seconds, which is what makes the gate exist. A bundler in the middle would break the ruler (quality gate) along\nwith the portability."]}),"\n",(0,i.jsx)(n.h2,{id:"content-data-system",children:"Content data system"}),"\n",(0,i.jsxs)(n.p,{children:["Today maps, weapons and characters are ",(0,i.jsx)(n.strong,{children:"code"}),": each ",(0,i.jsx)(n.code,{children:"map_*.js"}),' is geometry declared by\nhand, and the largest of them rival the system modules in size. The\n"content as data" direction in\n',(0,i.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,i.jsx)(n.code,{children:"docs/ROADMAP.md"})}),"\nwants to migrate this to JSON with a single loader, so that a content contribution becomes\n",(0,i.jsx)(n.em,{children:'"open a JSON and create content"'})," instead of ",(0,i.jsx)(n.em,{children:'"a risky hand-coded code PR"'}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["If you want the highest-leverage work in the entire project, this is it. See\n",(0,i.jsx)(n.a,{href:"./status",children:"Current state"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"generated-vs-not",children:"What is generated, and what is not"}),"\n",(0,i.jsx)(n.p,{children:"Two things in this repository are generated by script, and for the same reason:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Generated"}),(0,i.jsx)(n.th,{children:"Script"}),(0,i.jsx)(n.th,{children:"Gate"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"tools/eval/ARCH.md"})," \u2014 symbol\u2192line index and conflict table"]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"npm run arch:check"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["The numeric blocks of ",(0,i.jsx)(n.code,{children:"README.md"})," and of this documentation"]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tools/gen-docs.mjs"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"npm run docs:check"})," (in ",(0,i.jsx)(n.code,{children:"check:fast"}),")"]})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"The rule that separates what goes in and what stays out:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Derivable from the code?"})," It becomes a generated block, between markers, with ",(0,i.jsx)(n.code,{children:"--check"})," in the gate.\nCounts of lines, of characters, of weapons, of maps, of scripts, of invariants,\nversion, the ",(0,i.jsx)(n.code,{children:"package.json"})," script list, dependency version."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Not derivable?"})," Then it is a decision or an explanation \u2014 and it ",(0,i.jsx)(n.strong,{children:"must not contain a number that\nages"}),". Write it without the number, or cite the command that produces it. The gate's scoreboard,\nfor example, depends on which inputs exist on the machine: it lives pasted from a real\nrun in ",(0,i.jsx)(n.code,{children:"KNOWN-BUGS.md"}),", not repeated across five pages."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["And the reason ",(0,i.jsx)(n.code,{children:"--check"})," is in the gate, not merely available: ",(0,i.jsx)(n.strong,{children:"what does not become a ruler is\noptimized away."})," A generator nobody is forced to run goes stale in a week,\nand then the documentation is back to lying with the appearance of rigor \u2014 which is worse than lying without\nit."]}),"\n",(0,i.jsxs)(n.admonition,{title:"Where you put the new gate in the chain matters",type:"danger",children:[(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"check:fast"})," is a chain of ",(0,i.jsx)(n.code,{children:"&&"}),": the first error cuts off the rest. ",(0,i.jsx)(n.code,{children:"arch:check"})," has been\nred for days, so ",(0,i.jsx)(n.strong,{children:"every gate placed after it is born dead"})," \u2014 it runs zero times\nand nobody notices, because the output stops earlier. That is exactly what happened to the first\nversion of ",(0,i.jsx)(n.code,{children:"docs:check"}),", and it is the same failure mode as BUG-02 (the gate measuring the viewmodel\nfrom yesterday because the ",(0,i.jsx)(n.code,{children:"&&"})," cut off before the JSON was regenerated)."]}),(0,i.jsxs)(n.p,{children:["That is why ",(0,i.jsx)(n.code,{children:"docs:check"})," comes ",(0,i.jsx)(n.strong,{children:"before"})," ",(0,i.jsx)(n.code,{children:"arch:check"})," in ",(0,i.jsx)(n.code,{children:"package.json"}),", with the reason\nwritten in the ",(0,i.jsx)(n.code,{children:"//check:fast"})," key. When ",(0,i.jsx)(n.code,{children:"ARCH.md"})," is regenerated and ",(0,i.jsx)(n.code,{children:"arch:check"})," goes\ngreen again, the order stops mattering; until then, it matters."]})]}),"\n",(0,i.jsxs)(n.p,{children:["Pasting a new block is writing the marker and running ",(0,i.jsx)(n.code,{children:"npm run docs"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"{/* BEGIN:GERADO:BLOCK_NAME \u2014 n\xe3o edite \xe0 m\xe3o, rode `npm run docs` */}\n{/* END:GERADO:BLOCK_NAME */}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["(",(0,i.jsx)(n.code,{children:"BLOCK_NAME"})," is one of the keys of the ",(0,i.jsx)(n.code,{children:"BLOCOS"})," object at the top of ",(0,i.jsx)(n.code,{children:"gen-docs.mjs"}),". A\ndeclared block that nobody consumes becomes a loud warning in the output \u2014 an orphan block is dead code that\npretends to be documentation.)"]}),"\n",(0,i.jsxs)(n.p,{children:["In plain Markdown (",(0,i.jsx)(n.code,{children:"README.md"}),") the marker is an HTML comment (",(0,i.jsx)(n.code,{children:"\x3c!-- BEGIN:GERADO:\u2026 --\x3e"}),").\nIn the pages of this doc it is an ",(0,i.jsx)(n.strong,{children:"MDX"})," comment (",(0,i.jsx)(n.code,{children:"{/* \u2026 */}"}),"): Docusaurus 3 compiles ",(0,i.jsx)(n.code,{children:".md"}),"\nas MDX, and an HTML comment there is a parse error that takes down the build. The generator accepts both\nsyntaxes and preserves whichever it finds."]}),"\n",(0,i.jsxs)(n.h3,{id:"what-the-generator-does-not-solve",children:["What the generator does NOT solve: ",(0,i.jsx)(n.code,{children:"arquivo:linha"})," pointers in prose"]}),"\n",(0,i.jsxs)(n.p,{children:["A ",(0,i.jsx)(n.code,{children:"game.js:5361"})," written in the middle of a paragraph is the cheap version of the same defect \u2014 it\npoints to the wrong place at the first commit that touches the file. It cannot be generated (the\npointer is part of the sentence), but the ",(0,i.jsx)(n.strong,{children:"gross case can be detected"}),": a pointer that points\npast the end of the file."]}),"\n","\n",(0,i.jsxs)(n.p,{children:["No ",(0,i.jsx)(n.code,{children:"arquivo:linha"})," pointer in the docs points outside the file it cites. \u2713"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["This checks only the file's ",(0,i.jsx)(n.strong,{children:"bound"}),": a pointer that still fits but changed subject passes here. That is why the house doctrine is to declare the SYMBOL and leave the line to the generator \u2014 see ",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Block generated by ",(0,i.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,i.jsx)(n.code,{children:"sweep of "}),"arquivo",":linha",(0,i.jsx)(n.code,{children:" across README/STATUS/HANDOFF/KNOWN-BUGS/docs/docs/SKILL"})]}),"\n"]}),"\n","\n",(0,i.jsxs)(n.p,{children:["That is why the doctrine is to declare the ",(0,i.jsx)(n.strong,{children:"symbol"})," and leave the line to the generator. When the\n",(0,i.jsx)(n.code,{children:"arquivo:linha"})," really is necessary, cite alongside it the name of what lives there \u2014 that way whoever reads it\na month from now finds it via ",(0,i.jsx)(n.code,{children:"grep"})," even with the pointer shifted."]})]})}function a(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},8453(e,n,s){s.d(n,{R:()=>o,x:()=>d});var t=s(6540);const i={},r=t.createContext(i);function o(e){const n=t.useContext(r);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[3],{5805(e,n,s){s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>a,frontMatter:()=>o,metadata:()=>t,toc:()=>h});const t=JSON.parse('{"id":"arquitetura","title":"Architecture: N agents in the same file","description":"The real architecture, generated by tools/gen-arch.mjs \u2014 and the disjoint line-range mechanism that lets agents work in parallel without collision.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md","sourceDirName":".","slug":"/architecture","permalink":"/docs/en/architecture","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/arquitetura.md","tags":[],"version":"current","sidebarPosition":5,"frontMatter":{"id":"arquitetura","title":"Architecture: N agents in the same file","sidebar_label":"Architecture","sidebar_position":5,"slug":"/architecture","description":"The real architecture, generated by tools/gen-arch.mjs \u2014 and the disjoint line-range mechanism that lets agents work in parallel without collision."},"sidebar":"dev","previous":{"title":"BotBrain","permalink":"/docs/en/botbrain"},"next":{"title":"How to contribute","permalink":"/docs/en/contributing"}}');var i=s(4848),r=s(8453);const o={id:"arquitetura",title:"Architecture: N agents in the same file",sidebar_label:"Architecture",sidebar_position:5,slug:"/architecture",description:"The real architecture, generated by tools/gen-arch.mjs \u2014 and the disjoint line-range mechanism that lets agents work in parallel without collision."},d="Architecture: N agents in the same file",c={},h=[{value:"Why this document is generated by a script",id:"why-generated-by-script",level:2},{value:"The indexed files",id:"indexed-files",level:2},{value:"The largest methods in game.js \u2014 where the conflict lives",id:"largest-methods",level:3},{value:"Disjoint line ranges",id:"disjoint-line-ranges",level:2},{value:"How it works",id:"how-it-works",level:3},{value:"The conflict table",id:"conflict-table",level:3},{value:"The red zones",id:"red-zones",level:3},{value:"The operating rules",id:"operating-rules",level:3},{value:"The three zones of the repository",id:"three-zones",level:2},{value:"Practical consequence",id:"practical-consequence",level:3},{value:"Content data system",id:"content-data-system",level:2},{value:"What is generated, and what is not",id:"generated-vs-not",level:2},{value:"What the generator does NOT solve: arquivo:linha pointers in prose",id:"what-the-generator-does-not-solve",level:3}];function l(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:["\n",(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"architecture-n-agents-in-the-same-file",children:"Architecture: N agents in the same file"})}),"\n",(0,i.jsx)(n.h2,{id:"why-generated-by-script",children:"Why this document is generated by a script"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"tools/eval/ARCH.md"})," is not hand-written. It is generated by ",(0,i.jsx)(n.code,{children:"node tools/gen-arch.mjs"}),",\nand the reason is in the script's header (",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:5-8"}),"):"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["The hand-written ",(0,i.jsx)(n.code,{children:"ARCH.md"}),' said "game.js (3234 lines)" when the file had 5361.\nEvery ',(0,i.jsx)(n.code,{children:"arquivo:linha"})," pointer in the conflict table was off \u2014 and that\ntable is precisely what keeps two agents (or two contributors) from editing the\nsame region. A hand-written line-number index goes stale on the first\ncommit; the only fix is to generate it."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["And the separation that makes this work (",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:11-13"}),", quoted verbatim \u2014\nthe source comments are in Portuguese):"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"frente -> S\xcdMBOLO = conhecimento humano, est\xe1vel, vive nas FRENTES do script\ns\xedmbolo -> LINHA = vol\xe1til, \xe9 o que este script resolve toda vez\n"})}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:'Translation: "front \u2192 SYMBOL = human knowledge, stable, lives in the script\'s FRENTES;\nsymbol \u2192 LINE = volatile, it is what this script resolves every time."'})}),"\n",(0,i.jsxs)(n.p,{children:["The old ",(0,i.jsx)(n.code,{children:"ARCH.md"})," pinned ",(0,i.jsx)(n.strong,{children:"front \u2192 line"}),", mixing the two shelf lives. It is\na small idea with a big consequence: the work partition is declared in terms\nthat do not change (method names), and the resolution to volatile coordinates (line\nnumbers) is recomputed on every run."]}),"\n",(0,i.jsxs)(n.admonition,{type:"note",children:[(0,i.jsxs)(n.mdxAdmonitionTitle,{children:["The ",(0,i.jsx)(n.code,{children:"arch:check"})," is RED right now \u2014 and that is the best demonstration on this page"]}),(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"npm run arch"})," and ",(0,i.jsx)(n.code,{children:"npm run arch:check"})," exist today in the root ",(0,i.jsx)(n.code,{children:"package.json"}),", and the check\nis not passing. Gate output below, quoted verbatim \u2014 the tools print in Portuguese:"]}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"$ npm run arch:check\n\u2717 ARCH1 ARCH.md est\xe1 DESATUALIZADO em rela\xe7\xe3o ao c\xf3digo.\n game.js tem 6428 linhas; o \xedndice do ARCH.md n\xe3o bate.\n Rode: npm run arch\n"})}),(0,i.jsxs)(n.p,{children:["The message misleads on purpose: it ",(0,i.jsx)(n.strong,{children:"talks about lines because that is the summary it\nknows how to print"}),", but what ",(0,i.jsx)(n.code,{children:"--check"})," compares is the entire generated block, byte by byte \u2014 and that\nblock also carries the game's version number. Correct symbol index plus stale version gives\nthe same red. One command fixes it."]}),(0,i.jsxs)(n.p,{children:["A caveat that still holds: in CI the step has ",(0,i.jsx)(n.code,{children:"continue-on-error: true"}),", so the\ncheck runs but ",(0,i.jsx)(n.strong,{children:"does not block"})," \u2014 which is exactly how it managed to stay\nred without anyone noticing. Removing that line is what turns it into a real\ngate."]})]}),"\n",(0,i.jsx)(n.h2,{id:"indexed-files",children:"The indexed files"}),"\n",(0,i.jsxs)(n.p,{children:["Size of the files ",(0,i.jsx)(n.code,{children:"gen-arch.mjs"})," indexes \u2014 a generated block, regenerated by\n",(0,i.jsx)(n.code,{children:"npm run docs"})," and checked by ",(0,i.jsx)(n.code,{children:"npm run docs:check"}),":"]}),"\n","\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"File"}),(0,i.jsx)(n.th,{style:{textAlign:"right"},children:"Lines"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/game.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"6,910"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/main.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"2,698"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/characters.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"1,068"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/glbchars.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"844"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/vmattach.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"628"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/weapons.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"346"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"public/js/springs.js"})}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"260"})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:["Total in ",(0,i.jsx)(n.code,{children:"public/js/"}),": ",(0,i.jsx)(n.strong,{children:"32,001 lines in 44 files"}),". The symbol-to-line index lives in ",(0,i.jsx)(n.code,{children:"tools/eval/ARCH.md"}),"."]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Block generated by ",(0,i.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,i.jsx)(n.code,{children:"wc -l public/js/*.js"})]}),"\n"]}),"\n","\n",(0,i.jsxs)(n.h3,{id:"largest-methods",children:["The largest methods in ",(0,i.jsx)(n.code,{children:"game.js"})," \u2014 where the conflict lives"]}),"\n",(0,i.jsxs)(n.p,{children:["This table is ",(0,i.jsx)(n.strong,{children:"not reproduced here"}),", and the reason is this page's own thesis: it is\n",(0,i.jsx)(n.code,{children:"linha \u2192 m\xe9todo"}),", the volatile side of the separation, and duplicating it in a prose page creates a\nsecond copy that ages on its own. It lives generated, in one place only:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"npm run arch # regenerates tools/eval/ARCH.md\nnode tools/gen-arch.mjs --json # the raw index, for another tool\n"})}),"\n",(0,i.jsxs)(n.p,{children:["What does ",(0,i.jsx)(n.strong,{children:"not"})," age, and is therefore written here: ",(0,i.jsx)(n.code,{children:"_updateBot()"})," is by far the largest\nmethod in the file and is flagged by the index itself as an ",(0,i.jsx)(n.strong,{children:"extraction candidate"}),";\n",(0,i.jsx)(n.code,{children:"constructor()"}),", ",(0,i.jsx)(n.code,{children:"update()"})," and ",(0,i.jsx)(n.code,{children:"_dom()"})," are ",(0,i.jsx)(n.strong,{children:"red zone, append-only"}),", because any\nfront may need them. Big method = unreviewable PR and conflicting merge \u2014 extracting\n",(0,i.jsx)(n.code,{children:"_updateBot"})," is high-value, medium-risk work, and it requires coordinating first, because the\nregion is contested."]}),"\n",(0,i.jsx)(n.h2,{id:"disjoint-line-ranges",children:"Disjoint line ranges"}),"\n",(0,i.jsxs)(n.p,{children:["This is the mechanism that lets several agents (or contributors) edit the ",(0,i.jsx)(n.strong,{children:"same\nfile"})," \u2014 the largest in the repository, thousands of lines \u2014 at the same time, without merge\nconflict."]}),"\n",(0,i.jsx)(n.h3,{id:"how-it-works",children:"How it works"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Each front declares SYMBOLS, never lines."})," In ",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:32-73"}),", the\n",(0,i.jsx)(n.code,{children:"FRENTES"})," constant lists, per front, three things: exclusive ",(0,i.jsx)(n.code,{children:"arquivos"}),", ",(0,i.jsx)(n.code,{children:"simbolos"}),"\n(methods) and ",(0,i.jsx)(n.code,{children:"consts"})," (top-level constants). For example, the ARMAS/VIEWMODEL front owns\n",(0,i.jsx)(n.code,{children:"_buildViewModels"}),", ",(0,i.jsx)(n.code,{children:"_vmFrame"}),", ",(0,i.jsx)(n.code,{children:"_tryShoot"}),", ",(0,i.jsx)(n.code,{children:"_shotRecoil"}),"\u2026 and the constants ",(0,i.jsx)(n.code,{children:"WEAPONS"}),",\n",(0,i.jsx)(n.code,{children:"VM_FOV_DEFAULT"}),", ",(0,i.jsx)(n.code,{children:"VM_OFF"}),", ",(0,i.jsx)(n.code,{children:"REC_DEG"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"The script indexes the file and resolves symbol \u2192 range."})," ",(0,i.jsx)(n.code,{children:"indexar()"}),"\n(",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:80-111"}),") scans the file line by line with three patterns: class\nmethod (exactly 2 spaces of indentation), ",(0,i.jsx)(n.strong,{children:"arrow method assigned at runtime"}),"\n(",(0,i.jsx)(n.code,{children:"this._vmFrame = (force) => {"}),") and top-level declaration. Each symbol ends where the\nnext one begins."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Contiguous ranges are merged"})," (gap \u2264 12 lines) to keep the table readable\n(",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:172-178"}),")."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Overlap between fronts is detected"}),", because a conflict table that contradicts\nitself is worse than none (",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:190-200"}),")."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["The detail in step 2 deserves highlighting: v1 of the script only saw class methods, so\n",(0,i.jsx)(n.code,{children:"_vmFrame"})," \u2014 about 100 lines born ",(0,i.jsx)(n.strong,{children:"inside"})," another method, as an arrow that\ncloses over local variables \u2014 was ",(0,i.jsx)(n.strong,{children:"invisible in the index"}),"\n(",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:95-97"}),"). An index that cannot see the most contested method in the file is\nworse than no index at all, because it gives false confidence."]}),"\n",(0,i.jsx)(n.h3,{id:"conflict-table",children:"The conflict table"}),"\n",(0,i.jsxs)(n.p,{children:["From ",(0,i.jsx)(n.code,{children:"tools/eval/ARCH.md"})," (generated block \u2014 the ranges below are from the previous generation; run\n",(0,i.jsx)(n.code,{children:"node tools/gen-arch.mjs"})," for today's):"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Front"}),(0,i.jsx)(n.th,{children:"Exclusive files"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"ARMAS / VIEWMODEL"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"vmattach.js"})," ",(0,i.jsx)(n.code,{children:"springs.js"})," ",(0,i.jsx)(n.code,{children:"weapons.js"})," ",(0,i.jsx)(n.code,{children:"fparms.js"})," ",(0,i.jsx)(n.code,{children:"handik.js"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"BOTS / JOGABILIDADE"})}),(0,i.jsxs)(n.td,{children:["\u2014 (ranges in ",(0,i.jsx)(n.code,{children:"game.js"})," only)"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"MAPAS / MUNDO"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"maps.js"})," ",(0,i.jsx)(n.code,{children:"mapprops.js"})," ",(0,i.jsx)(n.code,{children:"map_brasilia.js"})," ",(0,i.jsx)(n.code,{children:"map_havan.js"})," ",(0,i.jsx)(n.code,{children:"map_piscina.js"})," ",(0,i.jsx)(n.code,{children:"map_piscinao_ramos.js"})," ",(0,i.jsx)(n.code,{children:"map_ferrovelho.js"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"GR\xc1FICOS / FX"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"bloom.js"})," ",(0,i.jsx)(n.code,{children:"textures.js"})," ",(0,i.jsx)(n.code,{children:"vao.js"})," ",(0,i.jsx)(n.code,{children:"stylize.js"})," ",(0,i.jsx)(n.code,{children:"gpuparticles.js"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"UI / HUD / MENU"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"main.js"})," ",(0,i.jsx)(n.code,{children:"public/style.css"})," ",(0,i.jsx)(n.code,{children:"src/pages/index.astro"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"\xc1UDIO"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"audio.js"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"PERSONAGENS"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"characters.js"})," ",(0,i.jsx)(n.code,{children:"glbchars.js"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"SITE / BACKEND"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"src/"})})]})]})]}),"\n",(0,i.jsxs)(n.admonition,{title:"Two map files have NO declared owner",type:"caution",children:[(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"map_quebrada.js"})," (1.319 lines, the newest map) and ",(0,i.jsx)(n.code,{children:"map_decals.js"})," ",(0,i.jsx)(n.strong,{children:"appear in no\nfront at all"})," in ",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs"})," \u2014 the list above is a faithful copy of ",(0,i.jsx)(n.code,{children:"FRENTES"}),", and they\nare not there. Whoever edits those two collides with nobody ",(0,i.jsx)(n.em,{children:"according to the table"}),", which is\nprecisely the guarantee the table is supposed to give and does not. Adding them is one line in\n",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs"})," followed by ",(0,i.jsx)(n.code,{children:"npm run arch"}),"."]}),(0,i.jsxs)(n.p,{children:["(",(0,i.jsx)(n.code,{children:"map.js"})," was once listed here and ",(0,i.jsx)(n.strong,{children:"no longer exists"}),': it was the "Pra\xe7a (cl\xe1ssico)", deleted\nalong with the ',(0,i.jsx)(n.code,{children:"praca_old"})," map.)"]})]}),"\n",(0,i.jsx)(n.h3,{id:"red-zones",children:"The red zones"}),"\n",(0,i.jsxs)(n.p,{children:["Three methods are ",(0,i.jsx)(n.strong,{children:"append-only"}),", because any front may need them\n(",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:75-77"}),"):"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"update()"})," \u2014 the loop"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"_dom()"})," \u2014 the HUD wiring"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"constructor()"})," \u2014 one of the largest methods in the file (today's size is in ",(0,i.jsx)(n.code,{children:"ARCH.md"}),")"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Editing the middle of these is the fastest way for two contributors to trample each other.\nAppend at the end; do not reorganize."}),"\n",(0,i.jsx)(n.h3,{id:"operating-rules",children:"The operating rules"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Declare your front before editing."})," If it is a human PR, say so in the description."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsxs)(n.strong,{children:["In ",(0,i.jsx)(n.code,{children:"game.js"}),", edit by chunk \u2014 never overwrite the whole file."]})," A\ntool that rewrites the file erases the work of whoever is on the other range."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Two fronts with disjoint ranges run in parallel."})," The generated ",(0,i.jsx)(n.code,{children:"ARCH.md"})," records\nthat this was measured: ",(0,i.jsx)(n.em,{children:'"3 agents edited disjoint ranges simultaneously with zero\ncontent conflict"'})," (",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs:163"}),")."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Touched a symbol? Move the name in the front's declaration, not the number."})," The script\nwarns when a declared symbol disappears from the code."]}),"\n"]}),"\n",(0,i.jsx)(n.admonition,{title:"Why this matters to you, human",type:"tip",children:(0,i.jsxs)(n.p,{children:["The same partition that prevents collisions between agents is what makes a PR of yours reviewable. A\nPR that touches ",(0,i.jsx)(n.code,{children:"_updateBot"})," + ",(0,i.jsx)(n.code,{children:"style.css"})," + ",(0,i.jsx)(n.code,{children:"map_havan.js"})," is three PRs hidden in one, and\nwill collide with three different fronts. One PR per front lands fast."]})}),"\n",(0,i.jsx)(n.h2,{id:"three-zones",children:"The three zones of the repository"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"public/ game vanilla ES modules, zero build, vendored Three.js\nsrc/ site Astro + Vercel adapter, SSR API routes\ntools/ harness .mjs/.py scripts \u2014 the ruler, the gate and the probes\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Versions, counts and what each tool does are in\n",(0,i.jsx)(n.a,{href:"/docs/en/stack",children:"Stack and tools"})," \u2014 ",(0,i.jsx)(n.strong,{children:"generated"}),", not hand-written."]}),"\n",(0,i.jsx)(n.p,{children:"The coupling between them is deliberately thin and worth understanding:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"The site loads the game via import map"}),", in ",(0,i.jsx)(n.code,{children:"src/pages/index.astro:97-123"}),". It is the only\nplace where Astro knows the game's modules exist."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"The harness loads the game straight from disk"}),", with no browser: ",(0,i.jsx)(n.code,{children:"tools/eval/harness.mjs"})," stubs\nDOM/canvas/",(0,i.jsx)(n.code,{children:"fetch"})," and imports ",(0,i.jsx)(n.code,{children:"public/js/game.js"})," as a module. That is why the gate measures\nproduction code, not a reimplementation."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:(0,i.jsx)(n.code,{children:"tools/eval/serve.mjs:15"})})," bridges to the test case: it serves ",(0,i.jsx)(n.code,{children:"public/"})," and maps\n",(0,i.jsx)(n.code,{children:"/"})," to the ",(0,i.jsx)(n.code,{children:"index.astro"})," source, with no Astro in the path."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"practical-consequence",children:"Practical consequence"}),"\n",(0,i.jsxs)(n.p,{children:["The game ",(0,i.jsx)(n.strong,{children:"cannot"})," gain a runtime dependency or a build step. This is not\nconservatism: it is what lets ",(0,i.jsx)(n.code,{children:"harness.mjs"})," boot the ",(0,i.jsx)(n.code,{children:"Game"})," class in pure node\nin seconds, which is what makes the gate exist. A bundler in the middle would break the ruler (quality gate) along\nwith the portability."]}),"\n",(0,i.jsx)(n.h2,{id:"content-data-system",children:"Content data system"}),"\n",(0,i.jsxs)(n.p,{children:["Today maps, weapons and characters are ",(0,i.jsx)(n.strong,{children:"code"}),": each ",(0,i.jsx)(n.code,{children:"map_*.js"}),' is geometry declared by\nhand, and the largest of them rival the system modules in size. The\n"content as data" direction in\n',(0,i.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,i.jsx)(n.code,{children:"docs/ROADMAP.md"})}),"\nwants to migrate this to JSON with a single loader, so that a content contribution becomes\n",(0,i.jsx)(n.em,{children:'"open a JSON and create content"'})," instead of ",(0,i.jsx)(n.em,{children:'"a risky hand-coded code PR"'}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["If you want the highest-leverage work in the entire project, this is it. See\n",(0,i.jsx)(n.a,{href:"./status",children:"Current state"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"generated-vs-not",children:"What is generated, and what is not"}),"\n",(0,i.jsx)(n.p,{children:"Two things in this repository are generated by script, and for the same reason:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Generated"}),(0,i.jsx)(n.th,{children:"Script"}),(0,i.jsx)(n.th,{children:"Gate"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"tools/eval/ARCH.md"})," \u2014 symbol\u2192line index and conflict table"]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"npm run arch:check"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["The numeric blocks of ",(0,i.jsx)(n.code,{children:"README.md"})," and of this documentation"]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tools/gen-docs.mjs"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"npm run docs:check"})," (in ",(0,i.jsx)(n.code,{children:"check:fast"}),")"]})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"The rule that separates what goes in and what stays out:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Derivable from the code?"})," It becomes a generated block, between markers, with ",(0,i.jsx)(n.code,{children:"--check"})," in the gate.\nCounts of lines, of characters, of weapons, of maps, of scripts, of invariants,\nversion, the ",(0,i.jsx)(n.code,{children:"package.json"})," script list, dependency version."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Not derivable?"})," Then it is a decision or an explanation \u2014 and it ",(0,i.jsx)(n.strong,{children:"must not contain a number that\nages"}),". Write it without the number, or cite the command that produces it. The gate's scoreboard,\nfor example, depends on which inputs exist on the machine: it lives pasted from a real\nrun in ",(0,i.jsx)(n.code,{children:"KNOWN-BUGS.md"}),", not repeated across five pages."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["And the reason ",(0,i.jsx)(n.code,{children:"--check"})," is in the gate, not merely available: ",(0,i.jsx)(n.strong,{children:"what does not become a ruler is\noptimized away."})," A generator nobody is forced to run goes stale in a week,\nand then the documentation is back to lying with the appearance of rigor \u2014 which is worse than lying without\nit."]}),"\n",(0,i.jsxs)(n.admonition,{title:"Where you put the new gate in the chain matters",type:"danger",children:[(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"check:fast"})," is a chain of ",(0,i.jsx)(n.code,{children:"&&"}),": the first error cuts off the rest. ",(0,i.jsx)(n.code,{children:"arch:check"})," has been\nred for days, so ",(0,i.jsx)(n.strong,{children:"every gate placed after it is born dead"})," \u2014 it runs zero times\nand nobody notices, because the output stops earlier. That is exactly what happened to the first\nversion of ",(0,i.jsx)(n.code,{children:"docs:check"}),", and it is the same failure mode as BUG-02 (the gate measuring the viewmodel\nfrom yesterday because the ",(0,i.jsx)(n.code,{children:"&&"})," cut off before the JSON was regenerated)."]}),(0,i.jsxs)(n.p,{children:["That is why ",(0,i.jsx)(n.code,{children:"docs:check"})," comes ",(0,i.jsx)(n.strong,{children:"before"})," ",(0,i.jsx)(n.code,{children:"arch:check"})," in ",(0,i.jsx)(n.code,{children:"package.json"}),", with the reason\nwritten in the ",(0,i.jsx)(n.code,{children:"//check:fast"})," key. When ",(0,i.jsx)(n.code,{children:"ARCH.md"})," is regenerated and ",(0,i.jsx)(n.code,{children:"arch:check"})," goes\ngreen again, the order stops mattering; until then, it matters."]})]}),"\n",(0,i.jsxs)(n.p,{children:["Pasting a new block is writing the marker and running ",(0,i.jsx)(n.code,{children:"npm run docs"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"{/* BEGIN:GERADO:BLOCK_NAME \u2014 n\xe3o edite \xe0 m\xe3o, rode `npm run docs` */}\n{/* END:GERADO:BLOCK_NAME */}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["(",(0,i.jsx)(n.code,{children:"BLOCK_NAME"})," is one of the keys of the ",(0,i.jsx)(n.code,{children:"BLOCOS"})," object at the top of ",(0,i.jsx)(n.code,{children:"gen-docs.mjs"}),". A\ndeclared block that nobody consumes becomes a loud warning in the output \u2014 an orphan block is dead code that\npretends to be documentation.)"]}),"\n",(0,i.jsxs)(n.p,{children:["In plain Markdown (",(0,i.jsx)(n.code,{children:"README.md"}),") the marker is an HTML comment (",(0,i.jsx)(n.code,{children:"\x3c!-- BEGIN:GERADO:\u2026 --\x3e"}),").\nIn the pages of this doc it is an ",(0,i.jsx)(n.strong,{children:"MDX"})," comment (",(0,i.jsx)(n.code,{children:"{/* \u2026 */}"}),"): Docusaurus 3 compiles ",(0,i.jsx)(n.code,{children:".md"}),"\nas MDX, and an HTML comment there is a parse error that takes down the build. The generator accepts both\nsyntaxes and preserves whichever it finds."]}),"\n",(0,i.jsxs)(n.h3,{id:"what-the-generator-does-not-solve",children:["What the generator does NOT solve: ",(0,i.jsx)(n.code,{children:"arquivo:linha"})," pointers in prose"]}),"\n",(0,i.jsxs)(n.p,{children:["A ",(0,i.jsx)(n.code,{children:"game.js:5361"})," written in the middle of a paragraph is the cheap version of the same defect \u2014 it\npoints to the wrong place at the first commit that touches the file. It cannot be generated (the\npointer is part of the sentence), but the ",(0,i.jsx)(n.strong,{children:"gross case can be detected"}),": a pointer that points\npast the end of the file."]}),"\n","\n",(0,i.jsxs)(n.p,{children:["No ",(0,i.jsx)(n.code,{children:"arquivo:linha"})," pointer in the docs points outside the file it cites. \u2713"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["This checks only the file's ",(0,i.jsx)(n.strong,{children:"bound"}),": a pointer that still fits but changed subject passes here. That is why the house doctrine is to declare the SYMBOL and leave the line to the generator \u2014 see ",(0,i.jsx)(n.code,{children:"tools/gen-arch.mjs"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Block generated by ",(0,i.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,i.jsx)(n.code,{children:"sweep of "}),"arquivo",":linha",(0,i.jsx)(n.code,{children:" across README/STATUS/HANDOFF/KNOWN-BUGS/docs/docs/SKILL"})]}),"\n"]}),"\n","\n",(0,i.jsxs)(n.p,{children:["That is why the doctrine is to declare the ",(0,i.jsx)(n.strong,{children:"symbol"})," and leave the line to the generator. When the\n",(0,i.jsx)(n.code,{children:"arquivo:linha"})," really is necessary, cite alongside it the name of what lives there \u2014 that way whoever reads it\na month from now finds it via ",(0,i.jsx)(n.code,{children:"grep"})," even with the pointer shifted."]})]})}function a(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},8453(e,n,s){s.d(n,{R:()=>o,x:()=>d});var t=s(6540);const i={},r=t.createContext(i);function o(e){const n=t.useContext(r);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/en/assets/js/a1985327.b0b84147.js b/public/docs/en/assets/js/a1985327.de92e49f.js similarity index 99% rename from public/docs/en/assets/js/a1985327.b0b84147.js rename to public/docs/en/assets/js/a1985327.de92e49f.js index 421f427bc..7d7dd617b 100644 --- a/public/docs/en/assets/js/a1985327.b0b84147.js +++ b/public/docs/en/assets/js/a1985327.de92e49f.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[515],{2102(e,n,s){s.r(n),s.d(n,{assets:()=>a,contentTitle:()=>d,default:()=>l,frontMatter:()=>o,metadata:()=>i,toc:()=>c});const i=JSON.parse('{"id":"colaborar","title":"How to contribute","description":"Setup, how to run the gate, what a PR needs, how to add a weapon / character / map, and the good first tasks.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md","sourceDirName":".","slug":"/contributing","permalink":"/docs/en/contributing","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/colaborar.md","tags":[],"version":"current","sidebarPosition":6,"frontMatter":{"id":"colaborar","title":"How to contribute","sidebar_label":"How to contribute","sidebar_position":6,"slug":"/contributing","description":"Setup, how to run the gate, what a PR needs, how to add a weapon / character / map, and the good first tasks."},"sidebar":"dev","previous":{"title":"Architecture","permalink":"/docs/en/architecture"},"next":{"title":"Current state","permalink":"/docs/en/status"}}');var t=s(4848),r=s(8453);const o={id:"colaborar",title:"How to contribute",sidebar_label:"How to contribute",sidebar_position:6,slug:"/contributing",description:"Setup, how to run the gate, what a PR needs, how to add a weapon / character / map, and the good first tasks."},d="How to contribute",a={},c=[{value:"Setup",id:"setup",level:2},{value:"Running the gate",id:"running-the-gate",level:2},{value:"Before saying you fixed it: mutate",id:"mutate-before-claiming",level:3},{value:"If your PR is a bug fix",id:"bug-fix-pr",level:3},{value:"What a PR needs",id:"what-a-pr-needs",level:2},{value:"1. A new invariant \u2014 or the reason it doesn't need one",id:"1-new-invariant",level:3},{value:"2. The gate cannot get worse",id:"2-gate-cannot-get-worse",level:3},{value:"3. Numbers, with arquivo:linha",id:"3-numbers-with-file-line",level:3},{value:"4. One front per PR",id:"4-one-front-per-pr",level:3},{value:"5. Repository hygiene",id:"5-repo-hygiene",level:3},{value:"6. Editorial line",id:"6-editorial-line",level:3},{value:"How to add a weapon",id:"how-to-add-a-weapon",level:2},{value:"How to add a character",id:"how-to-add-a-character",level:2},{value:"How to add a map",id:"how-to-add-a-map",level:2},{value:"Good first tasks",id:"good-first-tasks",level:2},{value:"Very good for the first PR",id:"very-good-first-pr",level:3},{value:"Real work, still accessible",id:"real-work-still-accessible",level:3},{value:"High value, needs a conversation first",id:"high-value-needs-conversation",level:3},{value:"Process",id:"process",level:2}];function h(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:["\n",(0,t.jsx)(n.header,{children:(0,t.jsx)(n.h1,{id:"how-to-contribute",children:"How to contribute"})}),"\n",(0,t.jsxs)(n.p,{children:["The number below is not rhetoric, and it is not hand-written: it comes from ",(0,t.jsx)(n.code,{children:"git shortlog -sn --no-merges"})," minus the authors that are AI agents (which sign as ",(0,t.jsx)(n.code,{children:"Claude"})," /\n",(0,t.jsx)(n.code,{children:"Claude (gauntlet \u2026)"}),")."]}),"\n","\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"11 human author identities"})," sign commits in this branch: ",(0,t.jsx)(n.code,{children:"ruben-cytonic"}),", ",(0,t.jsx)(n.code,{children:"Emerson Garrido"}),", ",(0,t.jsx)(n.code,{children:"Ruben"}),", ",(0,t.jsx)(n.code,{children:"rubenmarcus"}),", ",(0,t.jsx)(n.code,{children:"Ruben Marcus"}),", ",(0,t.jsx)(n.code,{children:"William Oliveira"}),", ",(0,t.jsx)(n.code,{children:"Juan Versolato Lopes"}),", ",(0,t.jsx)(n.code,{children:"daeeseD"}),", ",(0,t.jsx)(n.code,{children:"matheusgb"}),", ",(0,t.jsx)(n.code,{children:"Man\xe1 Soares"}),", ",(0,t.jsx)(n.code,{children:"daltonfontes"}),". Automated identities are excluded. A Git author name is not necessarily one unique person."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Block generated by ",(0,t.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,t.jsx)(n.code,{children:"git shortlog -sn --no-merges (descontando autores que s\xe3o agentes)"})]}),"\n"]}),"\n","\n",(0,t.jsx)(n.p,{children:"There is no team, there is no community, there is no reviewer queue \u2014 there are these\npeople and an automated gate."}),"\n",(0,t.jsx)(n.admonition,{title:"The block above counts the BRANCH, and the project is bigger than it",type:"note",children:(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"main"})," has a fourth contributor that this working branch does not contain \u2014 13 commits\nof a desktop client, merged in July. Who, how much, and why this matters for any\nlicensing decision is in ",(0,t.jsx)(n.code,{children:"CONTRIBUTING.md"})," (license section and surfaces)."]})}),"\n",(0,t.jsxs)(n.p,{children:["This is relevant to you in two opposite ways. The bad one: if your PR gets stuck, it can\ntake a while. The good one: ",(0,t.jsx)(n.strong,{children:"almost the entire ruler (quality gate) is machine."})," ",(0,t.jsx)(n.code,{children:"npm run check"})," gives\nyou the same verdict the maintainer would, before you open the PR, without waiting for\nanyone. The barrier is low ",(0,t.jsx)(n.strong,{children:"on purpose"})," \u2014 it is one of the principles that do not change in\n",(0,t.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,t.jsx)(n.code,{children:"docs/ROADMAP.md"})}),".\nBut the ruler is not."]}),"\n",(0,t.jsxs)(n.p,{children:["One-sentence summary: ",(0,t.jsx)(n.strong,{children:"bring the number."})," A PR that changes visible behavior and brings\nneither a new invariant nor the reason it does not need one will come back with a question."]}),"\n",(0,t.jsx)(n.h2,{id:"setup",children:"Setup"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"git clone https://github.com/rubenmarcus/csbrasil.git && cd csbrasil\nnpm install\nnpm run dev # http://localhost:4321 \u2014 the root route IS the game\n"})}),"\n",(0,t.jsx)(n.p,{children:"Optional:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run fetch-audio # audio pack (without it: synthesized sounds)\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Requirements: Node 22 (it is what the CI uses, ",(0,t.jsx)(n.code,{children:".github/workflows/ci.yml:14"}),") and Python 3 for\npart of the harness (",(0,t.jsx)(n.code,{children:"ref-measure.py"}),", ",(0,t.jsx)(n.code,{children:"char_probe.py"}),", ",(0,t.jsx)(n.code,{children:"mat_shade.py"})," \u2014 they use numpy and PIL)."]}),"\n",(0,t.jsxs)(n.admonition,{type:"caution",children:[(0,t.jsxs)(n.mdxAdmonitionTitle,{children:["Serving ",(0,t.jsx)(n.code,{children:"public/"})," does NOT run the game"]}),(0,t.jsxs)(n.p,{children:["There is no ",(0,t.jsx)(n.code,{children:"public/index.html"}),": the game's HTML is ",(0,t.jsx)(n.code,{children:"src/pages/index.astro"}),", at the root route.\nUse ",(0,t.jsx)(n.code,{children:"npm run dev"}),". Details and proof in\n",(0,t.jsx)(n.a,{href:"/docs/en/#the-first-hour-gotcha",children:"Getting started"}),"."]})]}),"\n",(0,t.jsx)(n.h2,{id:"running-the-gate",children:"Running the gate"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run eval:vm # MANDATORY FIRST \u2014 see the warning below\nnode tools/eval/invariants.mjs # the whole gate\nnode tools/eval/invariants.mjs --json # machine-readable output\nnpm run check # syntax + vm + gate + recoil + bots\n"})}),"\n",(0,t.jsxs)(n.admonition,{type:"danger",children:[(0,t.jsxs)(n.mdxAdmonitionTitle,{children:[(0,t.jsx)(n.code,{children:"eval:vm"})," runs BEFORE ",(0,t.jsx)(n.code,{children:"invariants.mjs"}),". Always."]}),(0,t.jsxs)(n.p,{children:["The viewmodel invariants (VM1\u2013VM19) ",(0,t.jsx)(n.strong,{children:"read"})," ",(0,t.jsx)(n.code,{children:"tools/eval/vm_mint_audit.json"}),", which is what\n",(0,t.jsx)(n.code,{children:"eval:vm"})," ",(0,t.jsx)(n.strong,{children:"writes"}),". Running the invariants with that JSON stale measures yesterday's\nviewmodel and ",(0,t.jsx)(n.strong,{children:"invents reds"}),": on 04/08/2026 the JSON was at ",(0,t.jsx)(n.code,{children:"V0=80\xb0"})," against ",(0,t.jsx)(n.code,{children:"game.js"}),"\nat ",(0,t.jsx)(n.code,{children:"V0=42\xb0"}),", and VM5 flagged ",(0,t.jsx)(n.strong,{children:"26/26 weapons out"}),"; after ",(0,t.jsx)(n.code,{children:"npm run eval:vm"}),", ",(0,t.jsx)(n.strong,{children:"3/26"}),".\nVM1 dropped from 26/26 to 2/26 and VM9 went green."]}),(0,t.jsxs)(n.p,{children:["The order of ",(0,t.jsx)(n.code,{children:"npm run check"})," has already been fixed (",(0,t.jsx)(n.code,{children:"package.json"}),") \u2014 the care is for when\nyou call ",(0,t.jsx)(n.code,{children:"node tools/eval/invariants.mjs"})," by hand. Details: BUG-02 in\n",(0,t.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/KNOWN-BUGS.md",children:(0,t.jsx)(n.code,{children:"KNOWN-BUGS.md"})}),"."]})]}),"\n",(0,t.jsxs)(n.p,{children:["Real cost: on a 2-CPU machine, ",(0,t.jsx)(n.strong,{children:"about 10 minutes"}),". It boots the real game five\ntimes (once per map), runs 60 s of bot simulation per map and audits every weapon GLB.\nRun it before opening the PR, not after receiving the review."]}),"\n",(0,t.jsx)(n.p,{children:"Individual harnesses, for when you want to iterate fast on a single front:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"node tools/eval/vm-mint-audit.mjs # viewmodel framing (the whole arsenal)\nnode tools/eval/vm-solve.mjs # does a feasible point exist for the VM invariants?\nnode tools/eval/vm-solve.mjs --atual # only the margins of the current config (instant)\nnode tools/eval/botsim.mjs 60 all # bot navigation, all maps, fixed seeds\nnode tools/eval/char-probe.mjs # characters (C1..C6)\nnode tools/eval/map-check.mjs all # map geometry (MAP1-MAP3, CTF1)\nnode tools/eval/mat-check.mjs # material/light/fog/texture\nnode tools/eval/pickup-check.mjs # is every pickup reachable?\nnode tools/eval/ui-check.mjs # UI1 contrast \xb7 UI2 clutter \xb7 UI3 dead area \xb7 UI4 rhythm\n"})}),"\n",(0,t.jsx)(n.h3,{id:"mutate-before-claiming",children:"Before saying you fixed it: mutate"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"MUT=ui1_ctf_scrim_fraco node tools/eval/ui-check.mjs # expects UI1 to go RED\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Undo your own fix and check that the gate ",(0,t.jsx)(n.strong,{children:"goes red"}),". If it stays green,\nwhat you measured is not what you fixed. It is the most expensive lesson in this repository and it\nhas a whole page: ",(0,t.jsx)(n.a,{href:"/docs/en/quality-gates#mutation-test",children:"Mutation test"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"bug-fix-pr",children:"If your PR is a bug fix"}),"\n",(0,t.jsxs)(n.p,{children:["Use the ",(0,t.jsx)(n.code,{children:"bug-hunt"})," skill (",(0,t.jsx)(n.code,{children:".claude/skills/bug-hunt/SKILL.md"}),"). It is the step-by-step of this\ndoctrine applied to defects \u2014 with the real case that bought each rule, the template for the\n",(0,t.jsx)(n.code,{children:"KNOWN-BUGS.md"})," entry and the one for the final report, including how to declare what you did ",(0,t.jsx)(n.strong,{children:"not"}),"\nverify. It works for agents and for people."]}),"\n",(0,t.jsx)(n.h2,{id:"what-a-pr-needs",children:"What a PR needs"}),"\n",(0,t.jsx)(n.h3,{id:"1-new-invariant",children:"1. A new invariant \u2014 or the reason it doesn't need one"}),"\n",(0,t.jsxs)(n.p,{children:["This is the rule that defines the project. Every PR that changes ",(0,t.jsx)(n.strong,{children:"observable behavior"})," brings\none of two things:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"A new invariant"})," in ",(0,t.jsx)(n.code,{children:"tools/eval/invariants.mjs"}),", with a ceiling that has provenance\n(reference file + measured pixel + script that reproduces it), or"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"One sentence in the PR description"}),' saying why it does not need one. Valid reasons: "it is\nalready covered by invariant X" (say which), "it is a refactor with no observable change \u2014 the\ngate gives the same score before and after" (paste both), "it is pure content (text,\nasset) with no game rule attached".']}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:'Invalid reason: "I tested it manually and it looked good".'}),"\n",(0,t.jsxs)(n.p,{children:["Why: ",(0,t.jsx)(n.strong,{children:"intent that does not become an invariant gets optimized away"}),". One round took the\ngate from 16/21 to 19/21 without loosening a single ceiling, and was rejected, because it\nsilently destroyed an aesthetic decision that no invariant encoded. Full case in\n",(0,t.jsx)(n.a,{href:"/docs/en/quality-gates#law-1",children:"The gate"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"2-gate-cannot-get-worse",children:"2. The gate cannot get worse"}),"\n",(0,t.jsxs)(n.p,{children:["Paste the output of ",(0,t.jsx)(n.code,{children:"node tools/eval/invariants.mjs"})," before and after. If any critical one went\nred, the PR does not go in. If you ",(0,t.jsx)(n.strong,{children:"fixed"})," a red one, say which and show it."]}),"\n",(0,t.jsxs)(n.p,{children:["The gate is red today (",(0,t.jsx)(n.a,{href:"./status",children:"see which ones"}),", and the living list with root causes is\nin ",(0,t.jsx)(n.code,{children:"KNOWN-BUGS.md"}),"). That is not a license to make it worse: the commitment is ",(0,t.jsx)(n.em,{children:'"your change\nadds no red"'}),"."]}),"\n",(0,t.jsxs)(n.h3,{id:"3-numbers-with-file-line",children:["3. Numbers, with ",(0,t.jsx)(n.code,{children:"arquivo:linha"})]}),"\n",(0,t.jsxs)(n.p,{children:['The claim "I improved the lighting" is not reviewable. "The ',(0,t.jsx)(n.code,{children:"praca_poderes"})," floor was 8 points\nof L* above the walls, cause at ",(0,t.jsx)(n.code,{children:"map_brasilia.js:NNN"}),', fixed to X" is. This\nrequirement is not style \u2014 it is what allows the next round to check your work.']}),"\n",(0,t.jsx)(n.h3,{id:"4-one-front-per-pr",children:"4. One front per PR"}),"\n",(0,t.jsxs)(n.p,{children:["Consult the conflict table (",(0,t.jsx)(n.a,{href:"/docs/en/architecture#conflict-table",children:"Architecture"}),"). A\nPR that touches weapons + UI + map is three hidden PRs, and will collide with three fronts. In\n",(0,t.jsx)(n.code,{children:"game.js"}),", edit by section; never overwrite the whole file."]}),"\n",(0,t.jsx)(n.h3,{id:"5-repo-hygiene",children:"5. Repository hygiene"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"node --check"})," on every file in ",(0,t.jsx)(n.code,{children:"public/js/"})," that you edited (the CI does this first,\n",(0,t.jsx)(n.code,{children:".github/workflows/ci.yml:19-20"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:["Comments ",(0,t.jsx)(n.strong,{children:"in Portuguese explaining the why"}),", not the what. It is the repo's culture and it is\nwhat survives the next handoff."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Never delete a provenance comment"})," in a cleanup PR. That long paragraph\nexplaining where the number 0,513 came from is what keeps the next round from repeating three\nlost days."]}),"\n",(0,t.jsxs)(n.li,{children:["Touched ",(0,t.jsx)(n.code,{children:"public/js/*.js"}),"? ",(0,t.jsxs)(n.strong,{children:["Bump the ",(0,t.jsx)(n.code,{children:"?v="})," on both sides"]})," \u2014 ",(0,t.jsx)(n.code,{children:"public/js/version.js"})," and the\nimport map of ",(0,t.jsx)(n.code,{children:"src/pages/index.astro"}),'. It has already cost days of "fixes that never arrived".']}),"\n",(0,t.jsxs)(n.li,{children:["Touched ",(0,t.jsx)(n.code,{children:"public/js/*.js"}),", ",(0,t.jsx)(n.code,{children:"maps.js"}),", ",(0,t.jsx)(n.code,{children:"characters.js"}),", or a dependency?\n",(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"npm run docs"})]})," and commit it along. ",(0,t.jsx)(n.code,{children:"docs:check"})," is in ",(0,t.jsx)(n.code,{children:"check:fast"})," and will\nfail \u2014 it takes less than a second and is what keeps the doc from lying again."]}),"\n",(0,t.jsxs)(n.li,{children:["No copyrighted assets. No committed ",(0,t.jsx)(n.code,{children:"service_role"})," key."]}),"\n",(0,t.jsx)(n.li,{children:"No runtime dependency in the game. Three.js is vendored; the game has to run by\ndragging the folder onto a static host."}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"6-editorial-line",children:"6. Editorial line"}),"\n",(0,t.jsxs)(n.p,{children:["From ",(0,t.jsx)(n.code,{children:"CONTRIBUTING.md:7-16"}),": the game has no political side (both teams have the same\nmechanics), does not incite hatred, does not use real people \u2014 only original archetypes, no gore.\nContributions that violate this are rejected. It is not bureaucracy: it is what protects the project\nfrom takedowns and from becoming something else."]}),"\n",(0,t.jsx)(n.h2,{id:"how-to-add-a-weapon",children:"How to add a weapon"}),"\n",(0,t.jsxs)(n.p,{children:["The pipeline is data-driven from the GLB. Weapon GLBs live in ",(0,t.jsx)(n.code,{children:"public/models/weapons/"}),"\n(the count is in the generated block of ",(0,t.jsx)(n.a,{href:"/docs/en/",children:"Getting started"}),")."]}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Put the GLB"})," at ",(0,t.jsx)(n.code,{children:"public/models/weapons/.glb"}),". Geometry only \u2014 the material comes\nfrom the pipeline (",(0,t.jsx)(n.code,{children:"MAT1"})," requires ",(0,t.jsx)(n.code,{children:"metallicFactor 1 / roughnessFactor 1"})," with a\nmetallicRoughness map, which is the standard for all the current ones)."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Declare the weapon"})," in ",(0,t.jsx)(n.code,{children:"public/js/weapons.js"}),". The fields the gate reads:","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"len"})," \u2014 length in meters. ",(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"ARM4"})," fails above 1,25 m"]})," outside bolt-action\nsnipers. It is the field that normalizes scale; it is not decoration."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"gripZ"})," \u2014 fraction of the length, counted ",(0,t.jsx)(n.strong,{children:"from the muzzle"}),", where the grip sits\n(ak/m4 use 0,62 \u2014 it lands on the trigger guard). It is what anchors the hand."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"vm"})," \u2014 mesh scale multiplier in the viewmodel. It exists because the ",(0,t.jsx)(n.code,{children:"m92"})," hit\n14,50% against VM18b's measured ceiling of 13,09%."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"scope: true"})," ",(0,t.jsx)(n.strong,{children:"requires"})," ",(0,t.jsx)(n.code,{children:"spreadScope"})," declared \u2014 that is ",(0,t.jsx)(n.code,{children:"ARM1"}),', and it exists because\nof the "sniper without zoom".']}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Run the auditor:"})," ",(0,t.jsx)(n.code,{children:"node tools/eval/vm-mint-audit.mjs"}),". It opens the GLB with its own\nparser, projects the viewmodel in both aspects and writes ",(0,t.jsx)(n.code,{children:"tools/eval/vm_mint_audit.json"}),".\n",(0,t.jsx)(n.strong,{children:"That JSON is versioned"})," \u2014 without it, VM1\u2013VM6/VM9/VM10 become SKIPPED, which is a gate\ngreen by absence of data (",(0,t.jsx)(n.code,{children:".github/workflows/ci.yml:24-27"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Run the gate."})," You will face VM1, VM3, VM5, VM9, VM12, VM16, VM18, VM18b,\nVM19 \u2014 nine framing invariants, all with a range measured on a reference frame. If it\ndoes not close, use ",(0,t.jsx)(n.code,{children:"node tools/eval/vm-solve.mjs"})," instead of tuning by eye:\nit reads the ceilings from ",(0,t.jsx)(n.code,{children:"invariants.mjs"})," itself and says whether a feasible point exists, or ",(0,t.jsx)(n.strong,{children:"which pair\nof invariants intersects empty and by how much"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Commit the updated ",(0,t.jsx)(n.code,{children:"vm_mint_audit.json"})]})," along with the rest."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"how-to-add-a-character",children:"How to add a character"}),"\n",(0,t.jsxs)(n.p,{children:["45 GLBs in ",(0,t.jsx)(n.code,{children:"public/models/characters/"}),", 44 measured by ",(0,t.jsx)(n.code,{children:"char-probe.mjs"}),"."]}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"GLB with a rig"}),", in bind pose, feet on the ground. ",(0,t.jsx)(n.code,{children:"CHR3"})," requires ",(0,t.jsx)(n.code,{children:"|bbox base| \u2264 0,01 m"})," in the\nbind pose ",(0,t.jsx)(n.strong,{children:"and in every clip"})," \u2014 the sign separates two defects: ",(0,t.jsx)(n.code,{children:"y < 0"})," is feet inside the\nground, ",(0,t.jsx)(n.code,{children:"y > 0"})," is a character floating in the air."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Declare it"})," in ",(0,t.jsx)(n.code,{children:"public/js/characters.js"})," / ",(0,t.jsx)(n.code,{children:"public/js/glbchars.js"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"node tools/eval/char-probe.mjs"}),"."]})," What it will demand:","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR1"}),' \u2014 anthropometric proportion and "balloon" index. Today it ',(0,t.jsx)(n.strong,{children:"is red for the\nentire cast"}),", so you are not the one who broke it; but do not make it worse."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR2"})," \u2014 body height within half a head hitbox (dispersion \u2264 0,15 m). Measured\n",(0,t.jsx)(n.strong,{children:"without accessories"}),": hat/hair/pole inflate the bbox and make the GLB path (the CHR2 evidence itself points at ",(0,t.jsx)(n.code,{children:"glbchars.js:319-322"}),")\nshrink the body."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR4"})," \u2014 no palm born buried inside the body."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR5"}),"/",(0,t.jsx)(n.code,{children:"CHR5B"})," \u2014 finish (normal + roughness + AO). CHR5B ",(0,t.jsx)(n.strong,{children:"went green on\n04/08"}),': it was the "three finish levels on the same screen" that the owner described, with a\ngood part of the cast lacking any surface map, and today there are zero characters without one.\nA new character ',(0,t.jsx)(n.strong,{children:"without"})," normal + roughness reopens the red \u2014 bring the maps."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR6"})," \u2014 no pair with the same silhouette (IoU \u2264 0,98)."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"how-to-add-a-map",children:"How to add a map"}),"\n",(0,t.jsxs)(n.p,{children:["Today maps are ",(0,t.jsx)(n.strong,{children:"code"}),", not data: every ",(0,t.jsx)(n.code,{children:"map_*.js"})," is geometry declared by hand, and the\nbiggest ones rival the system modules in size. Migrating this to JSON is the Phase 2\ncontent-as-data of\n",(0,t.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,t.jsx)(n.code,{children:"docs/ROADMAP.md"})}),", and it is the\nhighest-leverage contribution in the project."]}),"\n",(0,t.jsxs)(n.p,{children:["The registry, generated from the ",(0,t.jsx)(n.code,{children:"MAPS"})," of ",(0,t.jsx)(n.code,{children:"public/js/maps.js"}),":"]}),"\n","\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Id"}),(0,t.jsx)(n.th,{children:"Menu name"}),(0,t.jsx)(n.th,{children:"Opens in"}),(0,t.jsxs)(n.th,{children:["File in ",(0,t.jsx)(n.code,{children:"public/js/"})]}),(0,t.jsx)(n.th,{style:{textAlign:"right"},children:"Lines"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"praca_poderes"})}),(0,t.jsx)(n.td,{children:"Pra\xe7a dos Tr\xeas Poderes"}),(0,t.jsx)(n.td,{children:"rounds"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_brasilia.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"1,830"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"piscina_treta"})}),(0,t.jsx)(n.td,{children:"Piscina da Treta"}),(0,t.jsx)(n.td,{children:"rounds"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_piscina.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"810"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"loja_h"})}),(0,t.jsx)(n.td,{children:"Loja H (Estacionamento)"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_havan.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"1,964"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ferro_velho"})}),(0,t.jsx)(n.td,{children:"Ferro Velho do Z\xe9"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_ferrovelho.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"1,888"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"quebrada"})}),(0,t.jsx)(n.td,{children:"Quebrada (Rua do Baile)"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_quebrada.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"1,599"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"posto_treta"})}),(0,t.jsx)(n.td,{children:"Posto da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_posto.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"489"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"upa_24h"})}),(0,t.jsx)(n.td,{children:"UPA 24h da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_upa.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"288"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"obras_prefeitura"})}),(0,t.jsx)(n.td,{children:"Obras da Prefeitura"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_obras.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"240"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"atacadao_treta"})}),(0,t.jsx)(n.td,{children:"Atacad\xe3o da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_atacadao.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"255"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parque_treta"})}),(0,t.jsx)(n.td,{children:"Parque da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_parque.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"402"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"velho_oeste"})}),(0,t.jsx)(n.td,{children:"Velho Oeste da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_velho_oeste.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"433"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"penitenciaria"})}),(0,t.jsx)(n.td,{children:"Penitenci\xe1ria da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_penitenciaria.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"247"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"12 registered maps"})," - 2 open in rounds and 10 in capture. ",(0,t.jsx)(n.code,{children:"ctfMode"})," sets the initial mode; it does not lock it. There are 14 ",(0,t.jsx)(n.code,{children:"map_*.js"})," files on disk, so a file alone does ",(0,t.jsx)(n.strong,{children:"not"})," make a map playable."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Block generated by ",(0,t.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,t.jsx)(n.code,{children:"objeto MAPS de public/js/maps.js"})]}),"\n"]}),"\n","\n",(0,t.jsx)(n.p,{children:"Two warnings that cost time if you do not know them:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"praca_old"}),' ("Pra\xe7a (cl\xe1ssico)") NO longer exists.']})," It left the registry and\n",(0,t.jsx)(n.code,{children:"public/js/map.js"})," was deleted along with it (the owner's literal request: ",(0,t.jsx)(n.em,{children:'"let\'s delete\nclassic pra\xe7a"'}),"). If you find ",(0,t.jsx)(n.code,{children:"praca_old"})," in a ruler output, that output predates the\nremoval \u2014 it is the case of the table pasted in ",(0,t.jsx)(n.a,{href:"./status",children:"Current state"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"map_piscinao_ramos.js"})," exists on disk and is NOT in the registry"]}),' (it is the "Piscin\xe3o"\nversion, out of the menu). A map file in ',(0,t.jsx)(n.code,{children:"public/js/"})," does not imply a playable map; what decides is\nthe ",(0,t.jsx)(n.code,{children:"MAPS"})," object."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"To add a map in today's format:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Create ",(0,t.jsx)(n.code,{children:"public/js/map_.js"})]})," exporting a ",(0,t.jsx)(n.code,{children:"build()"})," function. Use\n",(0,t.jsx)(n.code,{children:"map_piscina.js"})," as a reference \u2014 it is the smallest of the registered ones (the table above has\neach one's size)."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Register it in ",(0,t.jsx)(n.code,{children:"public/js/maps.js:8-36"})]})," \u2014 display name, ",(0,t.jsx)(n.code,{children:"build"}),", and ",(0,t.jsx)(n.code,{children:"ctfMode: true"})," if\nthe geometry was drawn around flags. ",(0,t.jsx)(n.code,{children:"ctfMode"})," ",(0,t.jsx)(n.strong,{children:"opens"})," the map in capture;\nit does not lock it. ",(0,t.jsx)(n.code,{children:"ctfOnly"})," no longer ",(0,t.jsx)(n.strong,{children:"exists"}),": ",(0,t.jsx)(n.code,{children:"MOD1"})," fails any map that forces the\nmode. The player chooses."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"node tools/eval/map-check.mjs "}),"."]})," What it measures, all by raycast\nagainst the real world:","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"MAP1"}),' \u2014 no spawn and no walkable floor with the body inside solid geometry.\nCeiling = a 0,30 m step (above that it is not "stepping over", it is "being inside").']}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"MAP2"})," \u2014 each team spawns entirely on the same floor; respawn not visible from outside (measured with\nthe game's ",(0,t.jsx)(n.strong,{children:"own"})," ",(0,t.jsx)(n.code,{children:"_losClear"}),", the same function that decides whether the bot shoots you)."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"MAP3"})," \u2014 stairs within NBR 9077 / Blondel (riser 16\u201318 cm, tread 25\u201332 cm,\n2h+p 63\u201365 cm, width \u2265 1,20 m) ",(0,t.jsx)(n.strong,{children:"and"})," the navigation graph + the flood-fill climb up\nthem."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CTF1"})," \u2014 flags not collinear, \u2265 2 rays from the nearest spawn, none buried."]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"node tools/eval/pickup-check.mjs"})]})," (it feeds ",(0,t.jsx)(n.code,{children:"VM14"}),"): every pickup must\nbe reachable ",(0,t.jsx)(n.strong,{children:"on foot"}),", by flood-fill of real connectivity on a 0,25 m grid\nseeded at the spawns of both teams. It has already happened that weapons fell into the pool of\n",(0,t.jsx)(n.code,{children:"piscina_treta"})," with the gate reporting a gap of ",(0,t.jsx)(n.strong,{children:"0,0000 \u2014 GREEN"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"node tools/eval/botsim.mjs 60 "})]}),": the bots must navigate your map\nwithout getting stuck (",(0,t.jsx)(n.code,{children:"BOT3"})," stuck \u2264 4%), without walking sideways (",(0,t.jsx)(n.code,{children:"BOT1"}),") and without spinning in place (",(0,t.jsx)(n.code,{children:"BOT2"}),').\nA disconnected waypoint is the most common defect of a new map, and it has broken PRs before\n(it is the defect the "content as data" direction exists to kill).']}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"good-first-tasks",children:"Good first tasks"}),"\n",(0,t.jsx)(n.p,{children:"Ordered by (impact \xf7 effort). All are real, verified in this tree, and none\nrequires understanding the whole game."}),"\n",(0,t.jsx)(n.h3,{id:"very-good-first-pr",children:"Very good for the first PR"}),"\n",(0,t.jsxs)(n.p,{children:["The entry tasks live in ",(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/tree/main/docs/issues",children:(0,t.jsx)(n.code,{children:"docs/issues/"})})}),",\none per file, each with context, what to do, acceptance criteria and which files\nto touch. The ",(0,t.jsx)(n.code,{children:"README.md"})," there indexes by available time (30 min / 1 h / 2-3 h) and by area\n(SEO, UI, backend, CI). ",(0,t.jsxs)(n.strong,{children:["None of them requires touching ",(0,t.jsx)(n.code,{children:"public/js/*.js"})]}),", on purpose:\nit is the code where the gameplay agents work in parallel and where the conflict table\nof ",(0,t.jsx)(n.code,{children:"tools/eval/ARCH.md"})," rules."]}),"\n",(0,t.jsxs)(n.admonition,{title:"They are NOT open on GitHub yet",type:"caution",children:[(0,t.jsxs)(n.p,{children:["They exist as files, not as issues. There is a ready-made script \u2014\n",(0,t.jsx)(n.code,{children:"docs/issues/abrir-issues.sh"}),", with ",(0,t.jsx)(n.a,{href:"https://cli.github.com/",children:(0,t.jsx)(n.code,{children:"gh"})})," authenticated:"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"bash docs/issues/abrir-issues.sh --dry-run # prints title + labels, opens nothing\nbash docs/issues/abrir-issues.sh --labels # creates the 8 labels in use\nbash docs/issues/abrir-issues.sh # opens the 15\n"})}),(0,t.jsxs)(n.p,{children:["It is idempotent (it looks for an issue with the same title before creating one) and ",(0,t.jsx)(n.strong,{children:"has never been\nrun"}),": the repository belongs to the owner and opening an issue is an irreversible action under his name.\nIn other words, if you look for the tasks in the Issues tab, you will not find them \u2014 read the ",(0,t.jsx)(n.code,{children:".md"})," files."]})]}),"\n",(0,t.jsxs)(n.admonition,{title:"This list once had five items, and four were done",type:"note",children:[(0,t.jsxs)(n.p,{children:["It told you to fix the ",(0,t.jsx)(n.code,{children:"README.md"})," (done), add ",(0,t.jsx)(n.code,{children:"arch"}),"/",(0,t.jsx)(n.code,{children:"arch:check"})," to the\n",(0,t.jsx)(n.code,{children:"package.json"})," (they exist today), regenerate the ",(0,t.jsx)(n.code,{children:"ARCH.md"})," and make the ",(0,t.jsx)(n.code,{children:"tp-mount-probe"})," skip\nwhen ",(0,t.jsx)(n.code,{children:"public/models/anims/"})," was missing \u2014 a folder that ",(0,t.jsx)(n.strong,{children:"is versioned today"})," (438 files\nin ",(0,t.jsx)(n.code,{children:"git ls-files public/models/anims"}),"). A doc that tells you to do what has already been done burns\nsomeone's first contribution; that is why the list became a pointer to ",(0,t.jsx)(n.code,{children:"docs/issues/"}),",\nwhich is maintained."]}),(0,t.jsxs)(n.p,{children:["The only item from the old list that ",(0,t.jsx)(n.strong,{children:"still stands"})," \u2014 and is now fixed: the\nmessage of invariants PX1\u2013PX4 pointed to ",(0,t.jsx)(n.code,{children:"tools/eval/motion.mjs"}),', which never\nexisted in git (a phantom pointer). The skips now honestly declare "no dedicated\nharness (PX debt)": what runs in CI browsers today is ',(0,t.jsx)(n.code,{children:"portao-browser"})," (real game\nboot + graffiti + selection-screen silhouette), and a dedicated viewmodel\nharness remains open work."]})]}),"\n",(0,t.jsx)(n.h3,{id:"real-work-still-accessible",children:"Real work, still accessible"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"VM12 and VM1 on the specific weapons."})," VM12 fails on 5 of 52 measurements (worst ",(0,t.jsx)(n.code,{children:"famas"}),"@3:2\nat 0,660 against the 0,62 ceiling); VM1 on 2 of 26 (",(0,t.jsx)(n.code,{children:"famas"}),", ",(0,t.jsx)(n.code,{children:"uzi"}),"). They are per-weapon fixes,\nwith a measured range and ",(0,t.jsx)(n.code,{children:"vm-solve.mjs"})," available to prove feasibility. ",(0,t.jsx)(n.em,{children:"Front:\nARMAS/VIEWMODEL."})]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"BOT8 \u2014 bot with line of sight and not shooting."})," It is the cheapest debt on the list, and the\nroot cause is already found: ",(0,t.jsx)(n.code,{children:"game.js:5361"})," evaluates ",(0,t.jsx)(n.code,{children:"const hasTurn = \u2026 this._duelToken(b)"}),"\n",(0,t.jsx)(n.strong,{children:"every frame"}),', before any "can shoot" gate \u2014 and ',(0,t.jsx)(n.code,{children:"_duelToken"})," does not consult,\nit ",(0,t.jsx)(n.strong,{children:"reserves"})," the token. A bot that is reloading or has no firing line steals one of the 2 tokens and\nholds it; the others cross the field of view without firing. The fix is to move the call\ninside the ",(0,t.jsx)(n.code,{children:"if"}),". Measured in the last recorded run: ",(0,t.jsx)(n.strong,{children:"4 episodes, maximum silence\n4,23 s"})," \u2014 and note that it ",(0,t.jsx)(n.strong,{children:"got worse"})," since the baseline's 2,7 / 3,03 s, which makes\nit also a good A/B. ",(0,t.jsx)(n.em,{children:"Front: BOTS/JOGABILIDADE. Details: BUG-03."})]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Characters: proportion (CHR1) and surface maps."})," Beware of stale doc here: the\n",(0,t.jsx)(n.strong,{children:"CHR5B went GREEN"})," on 04/08 (the 27 of 44 characters without a surface map went to\n",(0,t.jsx)(n.strong,{children:"0 of 44"}),"), so that specific item ",(0,t.jsx)(n.strong,{children:"has already been done"})," \u2014 do not redo it. What is still\nred is CHR1/CHR3/CHR4, and the underlying cause is rig, not runtime (BUG-10). Read the\nKNOWN-BUGS before picking it up. ",(0,t.jsx)(n.em,{children:"Front: PERSONAGENS."})]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"setTimeout"})," not cleared in ",(0,t.jsx)(n.code,{children:"dispose()"})]})," \u2014 a leak between matches, pointed out in\n",(0,t.jsx)(n.code,{children:"RELATORIO-ANALISE.md:134"}),". ",(0,t.jsx)(n.strong,{children:"The line numbers in that report are stale"})," (the\n",(0,t.jsx)(n.code,{children:"game.js"})," has moved ~1.000 lines since then); find the current ones with\n",(0,t.jsx)(n.code,{children:"grep -n setTimeout public/js/game.js"})," and check which ones survive ",(0,t.jsx)(n.code,{children:"dispose()"}),". A good\nhygiene PR with a measurable effect on the heap. ",(0,t.jsxs)(n.em,{children:["Front: red zone ",(0,t.jsx)(n.code,{children:"constructor"}),"/",(0,t.jsx)(n.code,{children:"update"}),"\n\u2014 coordinate first."]})]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"high-value-needs-conversation",children:"High value, needs a conversation first"}),"\n",(0,t.jsxs)(n.ol,{start:"5",children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsxs)(n.strong,{children:["Extract ",(0,t.jsx)(n.code,{children:"_updateBot()"})," (772 lines)."]})," Marked as an extraction candidate by the\ngenerated index itself. It needs prior agreement on the partition, because the region is\ncontested."]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Maps as JSON (Phase 2)."})," Geometry, colliders, occluders, spawns, pickups and\nwaypoints as data, with a single loader and ",(0,t.jsx)(n.strong,{children:"waypoints validated by test"}),'. It is what\nturns "risky code PR" into "open a JSON". Open an issue first.']}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"A nightly CI job with a browser"})," to unblock PX1\u2013PX4. Four pixel invariants\nhave been skipped since forever."]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"process",children:"Process"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Big feature? ",(0,t.jsx)(n.strong,{children:"Open an issue first"})," (see ",(0,t.jsx)(n.code,{children:"IDEAS.md"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:["Fork + branch ",(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"v2/"})})," \u2014 ",(0,t.jsx)(n.code,{children:"v2/multiplayer"}),", ",(0,t.jsx)(n.code,{children:"v2/audio"}),", ",(0,t.jsx)(n.code,{children:"v2/ui-hud"}),". The prefix\nis the release cycle (top of ",(0,t.jsx)(n.code,{children:"CHANGELOG.md"}),"), and the convention was born from a concrete\nproblem: on 04/08 the working branch was still called ",(0,t.jsx)(n.code,{children:"feat/evio-feel"})," \u2014 the name of a\nJuly feature \u2014 with ",(0,t.jsx)(n.strong,{children:"143 commits"})," of different subjects piled up. A name that does not\nsay what the branch is becomes a dumping ground. (Source: ",(0,t.jsx)(n.code,{children:"CONTRIBUTING.md"}),".)"]}),"\n",(0,t.jsxs)(n.li,{children:["Run ",(0,t.jsx)(n.code,{children:"npm run check"}),". Paste the output into the PR."]}),"\n",(0,t.jsxs)(n.li,{children:["Small PR, one front, description with numbers and ",(0,t.jsx)(n.code,{children:"arquivo:linha"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["By contributing you license under whatever license the ",(0,t.jsx)(n.code,{children:"LICENSE"})," states at the moment of your\nPR."]})," What it is today and which files must change together in a swap: the license\nsection of ",(0,t.jsx)(n.code,{children:"CONTRIBUTING.md"}),". If this\nis decisive for you, read it before writing the first line."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Reporting a bug: what happened, what you expected, steps to reproduce, browser/OS and a\nscreenshot of the console (F12). And if the bug is behavioral, it will become an invariant \u2014 that is\nhow it never comes back (",(0,t.jsx)(n.code,{children:"tools/eval/invariants.mjs:20-21"}),")."]})]})}function l(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},8453(e,n,s){s.d(n,{R:()=>o,x:()=>d});var i=s(6540);const t={},r=i.createContext(t);function o(e){const n=i.useContext(r);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[515],{2102(e,n,s){s.r(n),s.d(n,{assets:()=>a,contentTitle:()=>d,default:()=>l,frontMatter:()=>o,metadata:()=>i,toc:()=>c});const i=JSON.parse('{"id":"colaborar","title":"How to contribute","description":"Setup, how to run the gate, what a PR needs, how to add a weapon / character / map, and the good first tasks.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md","sourceDirName":".","slug":"/contributing","permalink":"/docs/en/contributing","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/colaborar.md","tags":[],"version":"current","sidebarPosition":6,"frontMatter":{"id":"colaborar","title":"How to contribute","sidebar_label":"How to contribute","sidebar_position":6,"slug":"/contributing","description":"Setup, how to run the gate, what a PR needs, how to add a weapon / character / map, and the good first tasks."},"sidebar":"dev","previous":{"title":"Architecture","permalink":"/docs/en/architecture"},"next":{"title":"Current state","permalink":"/docs/en/status"}}');var t=s(4848),r=s(8453);const o={id:"colaborar",title:"How to contribute",sidebar_label:"How to contribute",sidebar_position:6,slug:"/contributing",description:"Setup, how to run the gate, what a PR needs, how to add a weapon / character / map, and the good first tasks."},d="How to contribute",a={},c=[{value:"Setup",id:"setup",level:2},{value:"Running the gate",id:"running-the-gate",level:2},{value:"Before saying you fixed it: mutate",id:"mutate-before-claiming",level:3},{value:"If your PR is a bug fix",id:"bug-fix-pr",level:3},{value:"What a PR needs",id:"what-a-pr-needs",level:2},{value:"1. A new invariant \u2014 or the reason it doesn't need one",id:"1-new-invariant",level:3},{value:"2. The gate cannot get worse",id:"2-gate-cannot-get-worse",level:3},{value:"3. Numbers, with arquivo:linha",id:"3-numbers-with-file-line",level:3},{value:"4. One front per PR",id:"4-one-front-per-pr",level:3},{value:"5. Repository hygiene",id:"5-repo-hygiene",level:3},{value:"6. Editorial line",id:"6-editorial-line",level:3},{value:"How to add a weapon",id:"how-to-add-a-weapon",level:2},{value:"How to add a character",id:"how-to-add-a-character",level:2},{value:"How to add a map",id:"how-to-add-a-map",level:2},{value:"Good first tasks",id:"good-first-tasks",level:2},{value:"Very good for the first PR",id:"very-good-first-pr",level:3},{value:"Real work, still accessible",id:"real-work-still-accessible",level:3},{value:"High value, needs a conversation first",id:"high-value-needs-conversation",level:3},{value:"Process",id:"process",level:2}];function h(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:["\n",(0,t.jsx)(n.header,{children:(0,t.jsx)(n.h1,{id:"how-to-contribute",children:"How to contribute"})}),"\n",(0,t.jsxs)(n.p,{children:["The number below is not rhetoric, and it is not hand-written: it comes from ",(0,t.jsx)(n.code,{children:"git shortlog -sn --no-merges"})," minus the authors that are AI agents (which sign as ",(0,t.jsx)(n.code,{children:"Claude"})," /\n",(0,t.jsx)(n.code,{children:"Claude (gauntlet \u2026)"}),")."]}),"\n","\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"11 human author identities"})," sign commits in this branch: ",(0,t.jsx)(n.code,{children:"ruben-cytonic"}),", ",(0,t.jsx)(n.code,{children:"Ruben"}),", ",(0,t.jsx)(n.code,{children:"Emerson Garrido"}),", ",(0,t.jsx)(n.code,{children:"rubenmarcus"}),", ",(0,t.jsx)(n.code,{children:"Ruben Marcus"}),", ",(0,t.jsx)(n.code,{children:"William Oliveira"}),", ",(0,t.jsx)(n.code,{children:"Juan Versolato Lopes"}),", ",(0,t.jsx)(n.code,{children:"daeeseD"}),", ",(0,t.jsx)(n.code,{children:"matheusgb"}),", ",(0,t.jsx)(n.code,{children:"Man\xe1 Soares"}),", ",(0,t.jsx)(n.code,{children:"daltonfontes"}),". Automated identities are excluded. A Git author name is not necessarily one unique person."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Block generated by ",(0,t.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,t.jsx)(n.code,{children:"git shortlog -sn --no-merges (descontando autores que s\xe3o agentes)"})]}),"\n"]}),"\n","\n",(0,t.jsx)(n.p,{children:"There is no team, there is no community, there is no reviewer queue \u2014 there are these\npeople and an automated gate."}),"\n",(0,t.jsx)(n.admonition,{title:"The block above counts the BRANCH, and the project is bigger than it",type:"note",children:(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"main"})," has a fourth contributor that this working branch does not contain \u2014 13 commits\nof a desktop client, merged in July. Who, how much, and why this matters for any\nlicensing decision is in ",(0,t.jsx)(n.code,{children:"CONTRIBUTING.md"})," (license section and surfaces)."]})}),"\n",(0,t.jsxs)(n.p,{children:["This is relevant to you in two opposite ways. The bad one: if your PR gets stuck, it can\ntake a while. The good one: ",(0,t.jsx)(n.strong,{children:"almost the entire ruler (quality gate) is machine."})," ",(0,t.jsx)(n.code,{children:"npm run check"})," gives\nyou the same verdict the maintainer would, before you open the PR, without waiting for\nanyone. The barrier is low ",(0,t.jsx)(n.strong,{children:"on purpose"})," \u2014 it is one of the principles that do not change in\n",(0,t.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,t.jsx)(n.code,{children:"docs/ROADMAP.md"})}),".\nBut the ruler is not."]}),"\n",(0,t.jsxs)(n.p,{children:["One-sentence summary: ",(0,t.jsx)(n.strong,{children:"bring the number."})," A PR that changes visible behavior and brings\nneither a new invariant nor the reason it does not need one will come back with a question."]}),"\n",(0,t.jsx)(n.h2,{id:"setup",children:"Setup"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"git clone https://github.com/rubenmarcus/csbrasil.git && cd csbrasil\nnpm install\nnpm run dev # http://localhost:4321 \u2014 the root route IS the game\n"})}),"\n",(0,t.jsx)(n.p,{children:"Optional:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run fetch-audio # audio pack (without it: synthesized sounds)\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Requirements: Node 22 (it is what the CI uses, ",(0,t.jsx)(n.code,{children:".github/workflows/ci.yml:14"}),") and Python 3 for\npart of the harness (",(0,t.jsx)(n.code,{children:"ref-measure.py"}),", ",(0,t.jsx)(n.code,{children:"char_probe.py"}),", ",(0,t.jsx)(n.code,{children:"mat_shade.py"})," \u2014 they use numpy and PIL)."]}),"\n",(0,t.jsxs)(n.admonition,{type:"caution",children:[(0,t.jsxs)(n.mdxAdmonitionTitle,{children:["Serving ",(0,t.jsx)(n.code,{children:"public/"})," does NOT run the game"]}),(0,t.jsxs)(n.p,{children:["There is no ",(0,t.jsx)(n.code,{children:"public/index.html"}),": the game's HTML is ",(0,t.jsx)(n.code,{children:"src/pages/index.astro"}),", at the root route.\nUse ",(0,t.jsx)(n.code,{children:"npm run dev"}),". Details and proof in\n",(0,t.jsx)(n.a,{href:"/docs/en/#the-first-hour-gotcha",children:"Getting started"}),"."]})]}),"\n",(0,t.jsx)(n.h2,{id:"running-the-gate",children:"Running the gate"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run eval:vm # MANDATORY FIRST \u2014 see the warning below\nnode tools/eval/invariants.mjs # the whole gate\nnode tools/eval/invariants.mjs --json # machine-readable output\nnpm run check # syntax + vm + gate + recoil + bots\n"})}),"\n",(0,t.jsxs)(n.admonition,{type:"danger",children:[(0,t.jsxs)(n.mdxAdmonitionTitle,{children:[(0,t.jsx)(n.code,{children:"eval:vm"})," runs BEFORE ",(0,t.jsx)(n.code,{children:"invariants.mjs"}),". Always."]}),(0,t.jsxs)(n.p,{children:["The viewmodel invariants (VM1\u2013VM19) ",(0,t.jsx)(n.strong,{children:"read"})," ",(0,t.jsx)(n.code,{children:"tools/eval/vm_mint_audit.json"}),", which is what\n",(0,t.jsx)(n.code,{children:"eval:vm"})," ",(0,t.jsx)(n.strong,{children:"writes"}),". Running the invariants with that JSON stale measures yesterday's\nviewmodel and ",(0,t.jsx)(n.strong,{children:"invents reds"}),": on 04/08/2026 the JSON was at ",(0,t.jsx)(n.code,{children:"V0=80\xb0"})," against ",(0,t.jsx)(n.code,{children:"game.js"}),"\nat ",(0,t.jsx)(n.code,{children:"V0=42\xb0"}),", and VM5 flagged ",(0,t.jsx)(n.strong,{children:"26/26 weapons out"}),"; after ",(0,t.jsx)(n.code,{children:"npm run eval:vm"}),", ",(0,t.jsx)(n.strong,{children:"3/26"}),".\nVM1 dropped from 26/26 to 2/26 and VM9 went green."]}),(0,t.jsxs)(n.p,{children:["The order of ",(0,t.jsx)(n.code,{children:"npm run check"})," has already been fixed (",(0,t.jsx)(n.code,{children:"package.json"}),") \u2014 the care is for when\nyou call ",(0,t.jsx)(n.code,{children:"node tools/eval/invariants.mjs"})," by hand. Details: BUG-02 in\n",(0,t.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/KNOWN-BUGS.md",children:(0,t.jsx)(n.code,{children:"KNOWN-BUGS.md"})}),"."]})]}),"\n",(0,t.jsxs)(n.p,{children:["Real cost: on a 2-CPU machine, ",(0,t.jsx)(n.strong,{children:"about 10 minutes"}),". It boots the real game five\ntimes (once per map), runs 60 s of bot simulation per map and audits every weapon GLB.\nRun it before opening the PR, not after receiving the review."]}),"\n",(0,t.jsx)(n.p,{children:"Individual harnesses, for when you want to iterate fast on a single front:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"node tools/eval/vm-mint-audit.mjs # viewmodel framing (the whole arsenal)\nnode tools/eval/vm-solve.mjs # does a feasible point exist for the VM invariants?\nnode tools/eval/vm-solve.mjs --atual # only the margins of the current config (instant)\nnode tools/eval/botsim.mjs 60 all # bot navigation, all maps, fixed seeds\nnode tools/eval/char-probe.mjs # characters (C1..C6)\nnode tools/eval/map-check.mjs all # map geometry (MAP1-MAP3, CTF1)\nnode tools/eval/mat-check.mjs # material/light/fog/texture\nnode tools/eval/pickup-check.mjs # is every pickup reachable?\nnode tools/eval/ui-check.mjs # UI1 contrast \xb7 UI2 clutter \xb7 UI3 dead area \xb7 UI4 rhythm\n"})}),"\n",(0,t.jsx)(n.h3,{id:"mutate-before-claiming",children:"Before saying you fixed it: mutate"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"MUT=ui1_ctf_scrim_fraco node tools/eval/ui-check.mjs # expects UI1 to go RED\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Undo your own fix and check that the gate ",(0,t.jsx)(n.strong,{children:"goes red"}),". If it stays green,\nwhat you measured is not what you fixed. It is the most expensive lesson in this repository and it\nhas a whole page: ",(0,t.jsx)(n.a,{href:"/docs/en/quality-gates#mutation-test",children:"Mutation test"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"bug-fix-pr",children:"If your PR is a bug fix"}),"\n",(0,t.jsxs)(n.p,{children:["Use the ",(0,t.jsx)(n.code,{children:"bug-hunt"})," skill (",(0,t.jsx)(n.code,{children:".claude/skills/bug-hunt/SKILL.md"}),"). It is the step-by-step of this\ndoctrine applied to defects \u2014 with the real case that bought each rule, the template for the\n",(0,t.jsx)(n.code,{children:"KNOWN-BUGS.md"})," entry and the one for the final report, including how to declare what you did ",(0,t.jsx)(n.strong,{children:"not"}),"\nverify. It works for agents and for people."]}),"\n",(0,t.jsx)(n.h2,{id:"what-a-pr-needs",children:"What a PR needs"}),"\n",(0,t.jsx)(n.h3,{id:"1-new-invariant",children:"1. A new invariant \u2014 or the reason it doesn't need one"}),"\n",(0,t.jsxs)(n.p,{children:["This is the rule that defines the project. Every PR that changes ",(0,t.jsx)(n.strong,{children:"observable behavior"})," brings\none of two things:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"A new invariant"})," in ",(0,t.jsx)(n.code,{children:"tools/eval/invariants.mjs"}),", with a ceiling that has provenance\n(reference file + measured pixel + script that reproduces it), or"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"One sentence in the PR description"}),' saying why it does not need one. Valid reasons: "it is\nalready covered by invariant X" (say which), "it is a refactor with no observable change \u2014 the\ngate gives the same score before and after" (paste both), "it is pure content (text,\nasset) with no game rule attached".']}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:'Invalid reason: "I tested it manually and it looked good".'}),"\n",(0,t.jsxs)(n.p,{children:["Why: ",(0,t.jsx)(n.strong,{children:"intent that does not become an invariant gets optimized away"}),". One round took the\ngate from 16/21 to 19/21 without loosening a single ceiling, and was rejected, because it\nsilently destroyed an aesthetic decision that no invariant encoded. Full case in\n",(0,t.jsx)(n.a,{href:"/docs/en/quality-gates#law-1",children:"The gate"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"2-gate-cannot-get-worse",children:"2. The gate cannot get worse"}),"\n",(0,t.jsxs)(n.p,{children:["Paste the output of ",(0,t.jsx)(n.code,{children:"node tools/eval/invariants.mjs"})," before and after. If any critical one went\nred, the PR does not go in. If you ",(0,t.jsx)(n.strong,{children:"fixed"})," a red one, say which and show it."]}),"\n",(0,t.jsxs)(n.p,{children:["The gate is red today (",(0,t.jsx)(n.a,{href:"./status",children:"see which ones"}),", and the living list with root causes is\nin ",(0,t.jsx)(n.code,{children:"KNOWN-BUGS.md"}),"). That is not a license to make it worse: the commitment is ",(0,t.jsx)(n.em,{children:'"your change\nadds no red"'}),"."]}),"\n",(0,t.jsxs)(n.h3,{id:"3-numbers-with-file-line",children:["3. Numbers, with ",(0,t.jsx)(n.code,{children:"arquivo:linha"})]}),"\n",(0,t.jsxs)(n.p,{children:['The claim "I improved the lighting" is not reviewable. "The ',(0,t.jsx)(n.code,{children:"praca_poderes"})," floor was 8 points\nof L* above the walls, cause at ",(0,t.jsx)(n.code,{children:"map_brasilia.js:NNN"}),', fixed to X" is. This\nrequirement is not style \u2014 it is what allows the next round to check your work.']}),"\n",(0,t.jsx)(n.h3,{id:"4-one-front-per-pr",children:"4. One front per PR"}),"\n",(0,t.jsxs)(n.p,{children:["Consult the conflict table (",(0,t.jsx)(n.a,{href:"/docs/en/architecture#conflict-table",children:"Architecture"}),"). A\nPR that touches weapons + UI + map is three hidden PRs, and will collide with three fronts. In\n",(0,t.jsx)(n.code,{children:"game.js"}),", edit by section; never overwrite the whole file."]}),"\n",(0,t.jsx)(n.h3,{id:"5-repo-hygiene",children:"5. Repository hygiene"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"node --check"})," on every file in ",(0,t.jsx)(n.code,{children:"public/js/"})," that you edited (the CI does this first,\n",(0,t.jsx)(n.code,{children:".github/workflows/ci.yml:19-20"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:["Comments ",(0,t.jsx)(n.strong,{children:"in Portuguese explaining the why"}),", not the what. It is the repo's culture and it is\nwhat survives the next handoff."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Never delete a provenance comment"})," in a cleanup PR. That long paragraph\nexplaining where the number 0,513 came from is what keeps the next round from repeating three\nlost days."]}),"\n",(0,t.jsxs)(n.li,{children:["Touched ",(0,t.jsx)(n.code,{children:"public/js/*.js"}),"? ",(0,t.jsxs)(n.strong,{children:["Bump the ",(0,t.jsx)(n.code,{children:"?v="})," on both sides"]})," \u2014 ",(0,t.jsx)(n.code,{children:"public/js/version.js"})," and the\nimport map of ",(0,t.jsx)(n.code,{children:"src/pages/index.astro"}),'. It has already cost days of "fixes that never arrived".']}),"\n",(0,t.jsxs)(n.li,{children:["Touched ",(0,t.jsx)(n.code,{children:"public/js/*.js"}),", ",(0,t.jsx)(n.code,{children:"maps.js"}),", ",(0,t.jsx)(n.code,{children:"characters.js"}),", or a dependency?\n",(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"npm run docs"})]})," and commit it along. ",(0,t.jsx)(n.code,{children:"docs:check"})," is in ",(0,t.jsx)(n.code,{children:"check:fast"})," and will\nfail \u2014 it takes less than a second and is what keeps the doc from lying again."]}),"\n",(0,t.jsxs)(n.li,{children:["No copyrighted assets. No committed ",(0,t.jsx)(n.code,{children:"service_role"})," key."]}),"\n",(0,t.jsx)(n.li,{children:"No runtime dependency in the game. Three.js is vendored; the game has to run by\ndragging the folder onto a static host."}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"6-editorial-line",children:"6. Editorial line"}),"\n",(0,t.jsxs)(n.p,{children:["From ",(0,t.jsx)(n.code,{children:"CONTRIBUTING.md:7-16"}),": the game has no political side (both teams have the same\nmechanics), does not incite hatred, does not use real people \u2014 only original archetypes, no gore.\nContributions that violate this are rejected. It is not bureaucracy: it is what protects the project\nfrom takedowns and from becoming something else."]}),"\n",(0,t.jsx)(n.h2,{id:"how-to-add-a-weapon",children:"How to add a weapon"}),"\n",(0,t.jsxs)(n.p,{children:["The pipeline is data-driven from the GLB. Weapon GLBs live in ",(0,t.jsx)(n.code,{children:"public/models/weapons/"}),"\n(the count is in the generated block of ",(0,t.jsx)(n.a,{href:"/docs/en/",children:"Getting started"}),")."]}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Put the GLB"})," at ",(0,t.jsx)(n.code,{children:"public/models/weapons/.glb"}),". Geometry only \u2014 the material comes\nfrom the pipeline (",(0,t.jsx)(n.code,{children:"MAT1"})," requires ",(0,t.jsx)(n.code,{children:"metallicFactor 1 / roughnessFactor 1"})," with a\nmetallicRoughness map, which is the standard for all the current ones)."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Declare the weapon"})," in ",(0,t.jsx)(n.code,{children:"public/js/weapons.js"}),". The fields the gate reads:","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"len"})," \u2014 length in meters. ",(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"ARM4"})," fails above 1,25 m"]})," outside bolt-action\nsnipers. It is the field that normalizes scale; it is not decoration."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"gripZ"})," \u2014 fraction of the length, counted ",(0,t.jsx)(n.strong,{children:"from the muzzle"}),", where the grip sits\n(ak/m4 use 0,62 \u2014 it lands on the trigger guard). It is what anchors the hand."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"vm"})," \u2014 mesh scale multiplier in the viewmodel. It exists because the ",(0,t.jsx)(n.code,{children:"m92"})," hit\n14,50% against VM18b's measured ceiling of 13,09%."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"scope: true"})," ",(0,t.jsx)(n.strong,{children:"requires"})," ",(0,t.jsx)(n.code,{children:"spreadScope"})," declared \u2014 that is ",(0,t.jsx)(n.code,{children:"ARM1"}),', and it exists because\nof the "sniper without zoom".']}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Run the auditor:"})," ",(0,t.jsx)(n.code,{children:"node tools/eval/vm-mint-audit.mjs"}),". It opens the GLB with its own\nparser, projects the viewmodel in both aspects and writes ",(0,t.jsx)(n.code,{children:"tools/eval/vm_mint_audit.json"}),".\n",(0,t.jsx)(n.strong,{children:"That JSON is versioned"})," \u2014 without it, VM1\u2013VM6/VM9/VM10 become SKIPPED, which is a gate\ngreen by absence of data (",(0,t.jsx)(n.code,{children:".github/workflows/ci.yml:24-27"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Run the gate."})," You will face VM1, VM3, VM5, VM9, VM12, VM16, VM18, VM18b,\nVM19 \u2014 nine framing invariants, all with a range measured on a reference frame. If it\ndoes not close, use ",(0,t.jsx)(n.code,{children:"node tools/eval/vm-solve.mjs"})," instead of tuning by eye:\nit reads the ceilings from ",(0,t.jsx)(n.code,{children:"invariants.mjs"})," itself and says whether a feasible point exists, or ",(0,t.jsx)(n.strong,{children:"which pair\nof invariants intersects empty and by how much"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Commit the updated ",(0,t.jsx)(n.code,{children:"vm_mint_audit.json"})]})," along with the rest."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"how-to-add-a-character",children:"How to add a character"}),"\n",(0,t.jsxs)(n.p,{children:["45 GLBs in ",(0,t.jsx)(n.code,{children:"public/models/characters/"}),", 44 measured by ",(0,t.jsx)(n.code,{children:"char-probe.mjs"}),"."]}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"GLB with a rig"}),", in bind pose, feet on the ground. ",(0,t.jsx)(n.code,{children:"CHR3"})," requires ",(0,t.jsx)(n.code,{children:"|bbox base| \u2264 0,01 m"})," in the\nbind pose ",(0,t.jsx)(n.strong,{children:"and in every clip"})," \u2014 the sign separates two defects: ",(0,t.jsx)(n.code,{children:"y < 0"})," is feet inside the\nground, ",(0,t.jsx)(n.code,{children:"y > 0"})," is a character floating in the air."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Declare it"})," in ",(0,t.jsx)(n.code,{children:"public/js/characters.js"})," / ",(0,t.jsx)(n.code,{children:"public/js/glbchars.js"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"node tools/eval/char-probe.mjs"}),"."]})," What it will demand:","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR1"}),' \u2014 anthropometric proportion and "balloon" index. Today it ',(0,t.jsx)(n.strong,{children:"is red for the\nentire cast"}),", so you are not the one who broke it; but do not make it worse."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR2"})," \u2014 body height within half a head hitbox (dispersion \u2264 0,15 m). Measured\n",(0,t.jsx)(n.strong,{children:"without accessories"}),": hat/hair/pole inflate the bbox and make the GLB path (the CHR2 evidence itself points at ",(0,t.jsx)(n.code,{children:"glbchars.js:319-322"}),")\nshrink the body."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR4"})," \u2014 no palm born buried inside the body."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR5"}),"/",(0,t.jsx)(n.code,{children:"CHR5B"})," \u2014 finish (normal + roughness + AO). CHR5B ",(0,t.jsx)(n.strong,{children:"went green on\n04/08"}),': it was the "three finish levels on the same screen" that the owner described, with a\ngood part of the cast lacking any surface map, and today there are zero characters without one.\nA new character ',(0,t.jsx)(n.strong,{children:"without"})," normal + roughness reopens the red \u2014 bring the maps."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CHR6"})," \u2014 no pair with the same silhouette (IoU \u2264 0,98)."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"how-to-add-a-map",children:"How to add a map"}),"\n",(0,t.jsxs)(n.p,{children:["Today maps are ",(0,t.jsx)(n.strong,{children:"code"}),", not data: every ",(0,t.jsx)(n.code,{children:"map_*.js"})," is geometry declared by hand, and the\nbiggest ones rival the system modules in size. Migrating this to JSON is the Phase 2\ncontent-as-data of\n",(0,t.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,t.jsx)(n.code,{children:"docs/ROADMAP.md"})}),", and it is the\nhighest-leverage contribution in the project."]}),"\n",(0,t.jsxs)(n.p,{children:["The registry, generated from the ",(0,t.jsx)(n.code,{children:"MAPS"})," of ",(0,t.jsx)(n.code,{children:"public/js/maps.js"}),":"]}),"\n","\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Id"}),(0,t.jsx)(n.th,{children:"Menu name"}),(0,t.jsx)(n.th,{children:"Opens in"}),(0,t.jsxs)(n.th,{children:["File in ",(0,t.jsx)(n.code,{children:"public/js/"})]}),(0,t.jsx)(n.th,{style:{textAlign:"right"},children:"Lines"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"praca_poderes"})}),(0,t.jsx)(n.td,{children:"Pra\xe7a dos Tr\xeas Poderes"}),(0,t.jsx)(n.td,{children:"rounds"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_brasilia.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"1,830"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"piscina_treta"})}),(0,t.jsx)(n.td,{children:"Piscina da Treta"}),(0,t.jsx)(n.td,{children:"rounds"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_piscina.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"810"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"loja_h"})}),(0,t.jsx)(n.td,{children:"Loja H (Estacionamento)"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_havan.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"1,964"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ferro_velho"})}),(0,t.jsx)(n.td,{children:"Ferro Velho do Z\xe9"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_ferrovelho.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"1,888"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"quebrada"})}),(0,t.jsx)(n.td,{children:"Quebrada (Rua do Baile)"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_quebrada.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"1,599"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"posto_treta"})}),(0,t.jsx)(n.td,{children:"Posto da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_posto.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"489"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"upa_24h"})}),(0,t.jsx)(n.td,{children:"UPA 24h da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_upa.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"288"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"obras_prefeitura"})}),(0,t.jsx)(n.td,{children:"Obras da Prefeitura"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_obras.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"240"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"atacadao_treta"})}),(0,t.jsx)(n.td,{children:"Atacad\xe3o da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_atacadao.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"255"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parque_treta"})}),(0,t.jsx)(n.td,{children:"Parque da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_parque.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"402"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"velho_oeste"})}),(0,t.jsx)(n.td,{children:"Velho Oeste da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_velho_oeste.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"433"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"penitenciaria"})}),(0,t.jsx)(n.td,{children:"Penitenci\xe1ria da Treta"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"capture"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"map_penitenciaria.js"})}),(0,t.jsx)(n.td,{style:{textAlign:"right"},children:"247"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"12 registered maps"})," - 2 open in rounds and 10 in capture. ",(0,t.jsx)(n.code,{children:"ctfMode"})," sets the initial mode; it does not lock it. There are 14 ",(0,t.jsx)(n.code,{children:"map_*.js"})," files on disk, so a file alone does ",(0,t.jsx)(n.strong,{children:"not"})," make a map playable."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Block generated by ",(0,t.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,t.jsx)(n.code,{children:"objeto MAPS de public/js/maps.js"})]}),"\n"]}),"\n","\n",(0,t.jsx)(n.p,{children:"Two warnings that cost time if you do not know them:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"praca_old"}),' ("Pra\xe7a (cl\xe1ssico)") NO longer exists.']})," It left the registry and\n",(0,t.jsx)(n.code,{children:"public/js/map.js"})," was deleted along with it (the owner's literal request: ",(0,t.jsx)(n.em,{children:'"let\'s delete\nclassic pra\xe7a"'}),"). If you find ",(0,t.jsx)(n.code,{children:"praca_old"})," in a ruler output, that output predates the\nremoval \u2014 it is the case of the table pasted in ",(0,t.jsx)(n.a,{href:"./status",children:"Current state"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"map_piscinao_ramos.js"})," exists on disk and is NOT in the registry"]}),' (it is the "Piscin\xe3o"\nversion, out of the menu). A map file in ',(0,t.jsx)(n.code,{children:"public/js/"})," does not imply a playable map; what decides is\nthe ",(0,t.jsx)(n.code,{children:"MAPS"})," object."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"To add a map in today's format:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Create ",(0,t.jsx)(n.code,{children:"public/js/map_.js"})]})," exporting a ",(0,t.jsx)(n.code,{children:"build()"})," function. Use\n",(0,t.jsx)(n.code,{children:"map_piscina.js"})," as a reference \u2014 it is the smallest of the registered ones (the table above has\neach one's size)."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Register it in ",(0,t.jsx)(n.code,{children:"public/js/maps.js:8-36"})]})," \u2014 display name, ",(0,t.jsx)(n.code,{children:"build"}),", and ",(0,t.jsx)(n.code,{children:"ctfMode: true"})," if\nthe geometry was drawn around flags. ",(0,t.jsx)(n.code,{children:"ctfMode"})," ",(0,t.jsx)(n.strong,{children:"opens"})," the map in capture;\nit does not lock it. ",(0,t.jsx)(n.code,{children:"ctfOnly"})," no longer ",(0,t.jsx)(n.strong,{children:"exists"}),": ",(0,t.jsx)(n.code,{children:"MOD1"})," fails any map that forces the\nmode. The player chooses."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"node tools/eval/map-check.mjs "}),"."]})," What it measures, all by raycast\nagainst the real world:","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"MAP1"}),' \u2014 no spawn and no walkable floor with the body inside solid geometry.\nCeiling = a 0,30 m step (above that it is not "stepping over", it is "being inside").']}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"MAP2"})," \u2014 each team spawns entirely on the same floor; respawn not visible from outside (measured with\nthe game's ",(0,t.jsx)(n.strong,{children:"own"})," ",(0,t.jsx)(n.code,{children:"_losClear"}),", the same function that decides whether the bot shoots you)."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"MAP3"})," \u2014 stairs within NBR 9077 / Blondel (riser 16\u201318 cm, tread 25\u201332 cm,\n2h+p 63\u201365 cm, width \u2265 1,20 m) ",(0,t.jsx)(n.strong,{children:"and"})," the navigation graph + the flood-fill climb up\nthem."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CTF1"})," \u2014 flags not collinear, \u2265 2 rays from the nearest spawn, none buried."]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"node tools/eval/pickup-check.mjs"})]})," (it feeds ",(0,t.jsx)(n.code,{children:"VM14"}),"): every pickup must\nbe reachable ",(0,t.jsx)(n.strong,{children:"on foot"}),", by flood-fill of real connectivity on a 0,25 m grid\nseeded at the spawns of both teams. It has already happened that weapons fell into the pool of\n",(0,t.jsx)(n.code,{children:"piscina_treta"})," with the gate reporting a gap of ",(0,t.jsx)(n.strong,{children:"0,0000 \u2014 GREEN"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Run ",(0,t.jsx)(n.code,{children:"node tools/eval/botsim.mjs 60 "})]}),": the bots must navigate your map\nwithout getting stuck (",(0,t.jsx)(n.code,{children:"BOT3"})," stuck \u2264 4%), without walking sideways (",(0,t.jsx)(n.code,{children:"BOT1"}),") and without spinning in place (",(0,t.jsx)(n.code,{children:"BOT2"}),').\nA disconnected waypoint is the most common defect of a new map, and it has broken PRs before\n(it is the defect the "content as data" direction exists to kill).']}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"good-first-tasks",children:"Good first tasks"}),"\n",(0,t.jsx)(n.p,{children:"Ordered by (impact \xf7 effort). All are real, verified in this tree, and none\nrequires understanding the whole game."}),"\n",(0,t.jsx)(n.h3,{id:"very-good-first-pr",children:"Very good for the first PR"}),"\n",(0,t.jsxs)(n.p,{children:["The entry tasks live in ",(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.a,{href:"https://github.com/rubenmarcus/csbrasil/tree/main/docs/issues",children:(0,t.jsx)(n.code,{children:"docs/issues/"})})}),",\none per file, each with context, what to do, acceptance criteria and which files\nto touch. The ",(0,t.jsx)(n.code,{children:"README.md"})," there indexes by available time (30 min / 1 h / 2-3 h) and by area\n(SEO, UI, backend, CI). ",(0,t.jsxs)(n.strong,{children:["None of them requires touching ",(0,t.jsx)(n.code,{children:"public/js/*.js"})]}),", on purpose:\nit is the code where the gameplay agents work in parallel and where the conflict table\nof ",(0,t.jsx)(n.code,{children:"tools/eval/ARCH.md"})," rules."]}),"\n",(0,t.jsxs)(n.admonition,{title:"They are NOT open on GitHub yet",type:"caution",children:[(0,t.jsxs)(n.p,{children:["They exist as files, not as issues. There is a ready-made script \u2014\n",(0,t.jsx)(n.code,{children:"docs/issues/abrir-issues.sh"}),", with ",(0,t.jsx)(n.a,{href:"https://cli.github.com/",children:(0,t.jsx)(n.code,{children:"gh"})})," authenticated:"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"bash docs/issues/abrir-issues.sh --dry-run # prints title + labels, opens nothing\nbash docs/issues/abrir-issues.sh --labels # creates the 8 labels in use\nbash docs/issues/abrir-issues.sh # opens the 15\n"})}),(0,t.jsxs)(n.p,{children:["It is idempotent (it looks for an issue with the same title before creating one) and ",(0,t.jsx)(n.strong,{children:"has never been\nrun"}),": the repository belongs to the owner and opening an issue is an irreversible action under his name.\nIn other words, if you look for the tasks in the Issues tab, you will not find them \u2014 read the ",(0,t.jsx)(n.code,{children:".md"})," files."]})]}),"\n",(0,t.jsxs)(n.admonition,{title:"This list once had five items, and four were done",type:"note",children:[(0,t.jsxs)(n.p,{children:["It told you to fix the ",(0,t.jsx)(n.code,{children:"README.md"})," (done), add ",(0,t.jsx)(n.code,{children:"arch"}),"/",(0,t.jsx)(n.code,{children:"arch:check"})," to the\n",(0,t.jsx)(n.code,{children:"package.json"})," (they exist today), regenerate the ",(0,t.jsx)(n.code,{children:"ARCH.md"})," and make the ",(0,t.jsx)(n.code,{children:"tp-mount-probe"})," skip\nwhen ",(0,t.jsx)(n.code,{children:"public/models/anims/"})," was missing \u2014 a folder that ",(0,t.jsx)(n.strong,{children:"is versioned today"})," (438 files\nin ",(0,t.jsx)(n.code,{children:"git ls-files public/models/anims"}),"). A doc that tells you to do what has already been done burns\nsomeone's first contribution; that is why the list became a pointer to ",(0,t.jsx)(n.code,{children:"docs/issues/"}),",\nwhich is maintained."]}),(0,t.jsxs)(n.p,{children:["The only item from the old list that ",(0,t.jsx)(n.strong,{children:"still stands"})," \u2014 and is now fixed: the\nmessage of invariants PX1\u2013PX4 pointed to ",(0,t.jsx)(n.code,{children:"tools/eval/motion.mjs"}),', which never\nexisted in git (a phantom pointer). The skips now honestly declare "no dedicated\nharness (PX debt)": what runs in CI browsers today is ',(0,t.jsx)(n.code,{children:"portao-browser"})," (real game\nboot + graffiti + selection-screen silhouette), and a dedicated viewmodel\nharness remains open work."]})]}),"\n",(0,t.jsx)(n.h3,{id:"real-work-still-accessible",children:"Real work, still accessible"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"VM12 and VM1 on the specific weapons."})," VM12 fails on 5 of 52 measurements (worst ",(0,t.jsx)(n.code,{children:"famas"}),"@3:2\nat 0,660 against the 0,62 ceiling); VM1 on 2 of 26 (",(0,t.jsx)(n.code,{children:"famas"}),", ",(0,t.jsx)(n.code,{children:"uzi"}),"). They are per-weapon fixes,\nwith a measured range and ",(0,t.jsx)(n.code,{children:"vm-solve.mjs"})," available to prove feasibility. ",(0,t.jsx)(n.em,{children:"Front:\nARMAS/VIEWMODEL."})]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"BOT8 \u2014 bot with line of sight and not shooting."})," It is the cheapest debt on the list, and the\nroot cause is already found: ",(0,t.jsx)(n.code,{children:"game.js:5361"})," evaluates ",(0,t.jsx)(n.code,{children:"const hasTurn = \u2026 this._duelToken(b)"}),"\n",(0,t.jsx)(n.strong,{children:"every frame"}),', before any "can shoot" gate \u2014 and ',(0,t.jsx)(n.code,{children:"_duelToken"})," does not consult,\nit ",(0,t.jsx)(n.strong,{children:"reserves"})," the token. A bot that is reloading or has no firing line steals one of the 2 tokens and\nholds it; the others cross the field of view without firing. The fix is to move the call\ninside the ",(0,t.jsx)(n.code,{children:"if"}),". Measured in the last recorded run: ",(0,t.jsx)(n.strong,{children:"4 episodes, maximum silence\n4,23 s"})," \u2014 and note that it ",(0,t.jsx)(n.strong,{children:"got worse"})," since the baseline's 2,7 / 3,03 s, which makes\nit also a good A/B. ",(0,t.jsx)(n.em,{children:"Front: BOTS/JOGABILIDADE. Details: BUG-03."})]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Characters: proportion (CHR1) and surface maps."})," Beware of stale doc here: the\n",(0,t.jsx)(n.strong,{children:"CHR5B went GREEN"})," on 04/08 (the 27 of 44 characters without a surface map went to\n",(0,t.jsx)(n.strong,{children:"0 of 44"}),"), so that specific item ",(0,t.jsx)(n.strong,{children:"has already been done"})," \u2014 do not redo it. What is still\nred is CHR1/CHR3/CHR4, and the underlying cause is rig, not runtime (BUG-10). Read the\nKNOWN-BUGS before picking it up. ",(0,t.jsx)(n.em,{children:"Front: PERSONAGENS."})]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"setTimeout"})," not cleared in ",(0,t.jsx)(n.code,{children:"dispose()"})]})," \u2014 a leak between matches, pointed out in\n",(0,t.jsx)(n.code,{children:"RELATORIO-ANALISE.md:134"}),". ",(0,t.jsx)(n.strong,{children:"The line numbers in that report are stale"})," (the\n",(0,t.jsx)(n.code,{children:"game.js"})," has moved ~1.000 lines since then); find the current ones with\n",(0,t.jsx)(n.code,{children:"grep -n setTimeout public/js/game.js"})," and check which ones survive ",(0,t.jsx)(n.code,{children:"dispose()"}),". A good\nhygiene PR with a measurable effect on the heap. ",(0,t.jsxs)(n.em,{children:["Front: red zone ",(0,t.jsx)(n.code,{children:"constructor"}),"/",(0,t.jsx)(n.code,{children:"update"}),"\n\u2014 coordinate first."]})]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"high-value-needs-conversation",children:"High value, needs a conversation first"}),"\n",(0,t.jsxs)(n.ol,{start:"5",children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsxs)(n.strong,{children:["Extract ",(0,t.jsx)(n.code,{children:"_updateBot()"})," (772 lines)."]})," Marked as an extraction candidate by the\ngenerated index itself. It needs prior agreement on the partition, because the region is\ncontested."]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Maps as JSON (Phase 2)."})," Geometry, colliders, occluders, spawns, pickups and\nwaypoints as data, with a single loader and ",(0,t.jsx)(n.strong,{children:"waypoints validated by test"}),'. It is what\nturns "risky code PR" into "open a JSON". Open an issue first.']}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"A nightly CI job with a browser"})," to unblock PX1\u2013PX4. Four pixel invariants\nhave been skipped since forever."]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"process",children:"Process"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Big feature? ",(0,t.jsx)(n.strong,{children:"Open an issue first"})," (see ",(0,t.jsx)(n.code,{children:"IDEAS.md"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:["Fork + branch ",(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"v2/"})})," \u2014 ",(0,t.jsx)(n.code,{children:"v2/multiplayer"}),", ",(0,t.jsx)(n.code,{children:"v2/audio"}),", ",(0,t.jsx)(n.code,{children:"v2/ui-hud"}),". The prefix\nis the release cycle (top of ",(0,t.jsx)(n.code,{children:"CHANGELOG.md"}),"), and the convention was born from a concrete\nproblem: on 04/08 the working branch was still called ",(0,t.jsx)(n.code,{children:"feat/evio-feel"})," \u2014 the name of a\nJuly feature \u2014 with ",(0,t.jsx)(n.strong,{children:"143 commits"})," of different subjects piled up. A name that does not\nsay what the branch is becomes a dumping ground. (Source: ",(0,t.jsx)(n.code,{children:"CONTRIBUTING.md"}),".)"]}),"\n",(0,t.jsxs)(n.li,{children:["Run ",(0,t.jsx)(n.code,{children:"npm run check"}),". Paste the output into the PR."]}),"\n",(0,t.jsxs)(n.li,{children:["Small PR, one front, description with numbers and ",(0,t.jsx)(n.code,{children:"arquivo:linha"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["By contributing you license under whatever license the ",(0,t.jsx)(n.code,{children:"LICENSE"})," states at the moment of your\nPR."]})," What it is today and which files must change together in a swap: the license\nsection of ",(0,t.jsx)(n.code,{children:"CONTRIBUTING.md"}),". If this\nis decisive for you, read it before writing the first line."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Reporting a bug: what happened, what you expected, steps to reproduce, browser/OS and a\nscreenshot of the console (F12). And if the bug is behavioral, it will become an invariant \u2014 that is\nhow it never comes back (",(0,t.jsx)(n.code,{children:"tools/eval/invariants.mjs:20-21"}),")."]})]})}function l(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},8453(e,n,s){s.d(n,{R:()=>o,x:()=>d});var i=s(6540);const t={},r=i.createContext(t);function o(e){const n=i.useContext(r);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/en/assets/js/c0b876b6.6d9b0018.js b/public/docs/en/assets/js/c0b876b6.fd1bf75e.js similarity index 84% rename from public/docs/en/assets/js/c0b876b6.6d9b0018.js rename to public/docs/en/assets/js/c0b876b6.fd1bf75e.js index 22f2acf31..51de21c8d 100644 --- a/public/docs/en/assets/js/c0b876b6.6d9b0018.js +++ b/public/docs/en/assets/js/c0b876b6.fd1bf75e.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[571],{6932(e,s,n){n.r(s),n.d(s,{assets:()=>o,contentTitle:()=>a,default:()=>j,frontMatter:()=>c,metadata:()=>t,toc:()=>l});const t=JSON.parse('{"id":"comecando","title":"What it is, and how to run it","description":"What CORO SOLTO is, how to run it in 3 commands, and the real repository structure \u2014 checked against the code.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/comecando.md","sourceDirName":".","slug":"/","permalink":"/docs/en/","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/comecando.md","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"id":"comecando","title":"What it is, and how to run it","sidebar_label":"Getting started","sidebar_position":1,"slug":"/","description":"What CORO SOLTO is, how to run it in 3 commands, and the real repository structure \u2014 checked against the code."},"sidebar":"dev","next":{"title":"Stack and tools","permalink":"/docs/en/stack"}}');var r=n(4848),i=n(8453),d=n(6025);const c={id:"comecando",title:"What it is, and how to run it",sidebar_label:"Getting started",sidebar_position:1,slug:"/",description:"What CORO SOLTO is, how to run it in 3 commands, and the real repository structure \u2014 checked against the code."},a="What it is, and how to run it",o={},l=[{value:"Run it in 3 commands",id:"run-in-3-commands",level:2},{value:"Linux, WebGL, and compatibility mode",id:"linux-webgl-and-compatibility-mode",level:3},{value:"Alternative without Astro (zero build dependency)",id:"alternative-without-astro",level:3},{value:"The gotcha that costs everyone their first hour",id:"the-first-hour-gotcha",level:2},{value:"The real repository structure",id:"real-repository-structure",level:2},{value:"The two zones",id:"the-two-zones",level:3},{value:"Commands you will use",id:"commands-you-will-use",level:2},{value:"Where to go now",id:"where-to-go-now",level:2}];function h(e){const s={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:["\n","\n","\n",(0,r.jsx)("div",{className:"cs-hero",children:(0,r.jsx)("img",{className:"cs-hero__bird",src:(0,d.Ay)("/img/canarinho-header.webp"),alt:"CORO SOLTO: Treta Suprema \u2014 the canarinho, the game's mascot, spinning",width:"604",height:"240"})}),"\n",(0,r.jsx)(s.header,{children:(0,r.jsx)(s.h1,{id:"what-it-is-and-how-to-run-it",children:"What it is, and how to run it"})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"CORO SOLTO: Treta Suprema"})," (formerly CS BRASIL) is a browser FPS written in\nvanilla JavaScript on top of Three.js r160, in the style of Counter-Strike 1.6: rounds,\nbots, AWP, Tab scoreboard, voice radio. It runs from a link, with nothing to install."]}),"\n",(0,r.jsxs)(s.p,{children:["The numbers below ",(0,r.jsx)(s.strong,{children:"are not hand-written"}),": they are regenerated by\n",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"})," from the code, and ",(0,r.jsx)(s.code,{children:"npm run docs:check"})," (inside\n",(0,r.jsx)(s.code,{children:"check:fast"}),") fails the gate when any of them diverges from the tree. Before that,\nthis page was aging at the very first commit \u2014 see\n",(0,r.jsx)(s.a,{href:"/docs/en/architecture#generated-vs-not",children:"what is generated, and what is not"}),"."]}),"\n","\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"What"}),(0,r.jsx)(s.th,{style:{textAlign:"right"},children:"How much"}),(0,r.jsx)(s.th,{children:"Where to check"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Game code"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"31,744 lines in 44 files"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files public/js/*.js | xargs wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"game.js"})}),(0,r.jsxs)(s.td,{style:{textAlign:"right"},children:[(0,r.jsx)(s.strong,{children:"6,838"})," lines"]}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"wc -l public/js/game.js"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"main.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"2,646 lines"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"wc -l public/js/main.js"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Weapons with GLB"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"26"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'public/models/weapons/*.glb' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Character GLBs"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"45"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'public/models/characters/*.glb' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Props in GLB"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"108"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'public/models/props/*.glb' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Versioned animation clips"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"573"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files public/models/anims | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Playable characters"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"44, in 5 factions"}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"CHARACTERS"})," array in ",(0,r.jsx)(s.code,{children:"characters.js"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Maps in the registry"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"12"}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"MAPS"})," object in ",(0,r.jsx)(s.code,{children:"maps.js"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Visual harnesses in HTML"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"15"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'public/*.html' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Harness scripts"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"192"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Pipeline scripts"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"54"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'tools/*.mjs' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Written entry tasks"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"26"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'docs/issues/[0-9]*.md' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Version"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:(0,r.jsx)(s.code,{children:"2.0.0-alpha.169"})}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"public/js/version.js"})," and ",(0,r.jsx)(s.code,{children:"package.json"})," (match)"]})]})]})]}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Block generated by ",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(s.code,{children:"the command in the right column of each row"})]}),"\n"]}),"\n","\n",(0,r.jsxs)(s.p,{children:["And the match rules that move around the most, all read from the constants in\n",(0,r.jsx)(s.code,{children:"public/js/game.js"}),":"]}),"\n","\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Rule"}),(0,r.jsx)(s.th,{children:"Value"}),(0,r.jsx)(s.th,{children:"Constant"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Factions \xb7 characters"}),(0,r.jsx)(s.td,{children:"5 \xb7 44 (B 9 \xb7 C 9 \xb7 E 8 \xb7 F 9 \xb7 U 9)"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"CHARACTERS"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Maps in the menu"}),(0,r.jsxs)(s.td,{children:["12 - 2 open in rounds, ",(0,r.jsx)(s.strong,{children:"10 in capture"})]}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"MAPS"})," / ",(0,r.jsx)(s.code,{children:"ctfMode"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Respawn"}),(0,r.jsx)(s.td,{children:"2.2 s"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"RESPAWN_DELAY"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Round"}),(0,r.jsx)(s.td,{children:"99 s, 3 wins"}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ROUND_TIME"})," / ",(0,r.jsx)(s.code,{children:"ROUNDS_TO_WIN"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Capture"}),(0,r.jsxs)(s.td,{children:["target = ",(0,r.jsx)(s.strong,{children:"all flags on the map"}),", 2 rounds (480 s safety net)"]}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"capsToWin = ctfPts.length"})," / ",(0,r.jsx)(s.code,{children:"CTF_ROUNDS_TO_WIN"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Health regeneration"}),(0,r.jsx)(s.td,{children:(0,r.jsxs)(s.strong,{children:["OFF - ",(0,r.jsx)(s.code,{children:"?regen=1"})," turns it back on"]})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"REGEN"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:["Ranking / ",(0,r.jsx)(s.code,{children:"/u/"})," pages"]}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"OFF - controlled by one flag"})}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"RANKING_ON"})," in ",(0,r.jsx)(s.code,{children:"src/lib/site.ts"})]})]})]})]}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Block generated by ",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(s.code,{children:"constantes de public/js/game.js \xb7 RANKING_ON de src/lib/site.ts"})]}),"\n"]}),"\n","\n",(0,r.jsxs)(s.p,{children:["The menu accepts from ",(0,r.jsx)(s.strong,{children:"2\xd72 to 8\xd78"})," bots (the engine accepts 1 to 8 per side); the default is 4\xd74."]}),"\n",(0,r.jsxs)(s.admonition,{title:"Two of these are a recent choice, not a defect",type:"note",children:[(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Health regeneration was turned off"})," on 05/08 (",(0,r.jsx)(s.code,{children:"REGEN = QS.get('regen') === '1'"}),"). It\nexisted, CoD-style \u2014 6 s without taking damage and 22 HP/s \u2014 and the owner reported it as\na bug (",(0,r.jsx)(s.em,{children:"\"the 1st player's health goes back to 100, I don't know why\""}),") precisely because\nit was ",(0,r.jsx)(s.strong,{children:"invisible"}),": no icon, no sound, no line in the settings. A rule the player does\nnot notice is indistinguishable from a defect. It remains fully intact behind ",(0,r.jsx)(s.code,{children:"?regen=1"}),",\nwith player\u2194bot symmetry. ",(0,r.jsx)(s.strong,{children:"Whoever turns it back on must ship the feedback along with\nit"})," \u2014 and solve what it had been papering over: with no healing, medkit, or armor, every\nlife after first contact was already lost."]}),(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"The ranking was turned off"})," and replaced with anonymous telemetry. ",(0,r.jsx)(s.code,{children:"/ranking"})," and ",(0,r.jsx)(s.code,{children:"/u/*"}),"\nrespond ",(0,r.jsxs)(s.strong,{children:["200 with a notice + ",(0,r.jsx)(s.code,{children:"noindex"})]})," (not 404 \u2014 the URLs are indexed and will come back),\nand ",(0,r.jsx)(s.code,{children:"/api/leaderboard"})," responds ",(0,r.jsx)(s.code,{children:"{disabled:true}"}),"."]})]}),"\n",(0,r.jsxs)(s.admonition,{title:"The gate is NOT green, and that is declared",type:"caution",children:[(0,r.jsxs)(s.p,{children:["How many invariants pass ",(0,r.jsx)(s.strong,{children:"is not derivable from the code"})," \u2014 it is the result of a run,\nand it even depends on which inputs exist on the machine. That is why that scoreboard is\nnot repeated here: it lives in the header of\n",(0,r.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/KNOWN-BUGS.md",children:(0,r.jsx)(s.code,{children:"KNOWN-BUGS.md"})}),", pasted\nfrom a real run, with the list of red ones, root cause, and ",(0,r.jsx)(s.code,{children:"arquivo:linha"})," for each one.\nThat is the file maintained day by day."]}),(0,r.jsx)(s.p,{children:"For today's state, run \u2014 do not repeat a number from memory:"}),(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"npm run eval:vm && node tools/eval/invariants.mjs --json # 10-12 min\n"})}),(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Order matters"}),": a viewmodel invariant measured with yesterday's JSON invents a red\n(see ",(0,r.jsx)(s.a,{href:"/docs/en/contributing#running-the-gate",children:"How to contribute"}),")."]})]}),"\n",(0,r.jsx)(s.h2,{id:"run-in-3-commands",children:"Run it in 3 commands"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"git clone https://github.com/rubenmarcus/csbrasil.git && cd csbrasil\nnpm install\nnpm run dev # opens http://localhost:4321 \u2014 this page IS the game\n"})}),"\n",(0,r.jsxs)(s.p,{children:["The audio pack (",(0,r.jsx)(s.code,{children:"npm run fetch-audio"}),") is ",(0,r.jsx)(s.strong,{children:"optional"}),": without it the game uses\nsynthesized sounds. The ",(0,r.jsx)(s.code,{children:"public/audio/"})," folder is not versioned."]}),"\n",(0,r.jsx)(s.h3,{id:"linux-webgl-and-compatibility-mode",children:"Linux, WebGL, and compatibility mode"}),"\n",(0,r.jsx)(s.p,{children:"The game tries WebGL2 and WebGL1, starting with the browser default and reducing\nantialiasing, GPU preference, and stencil before giving up. WebGL1, llvmpipe/SwiftShader,\nor another degraded tier automatically uses low quality for that session: DPR 0.75, no\nbloom or shadows, and static portraits in character selection."}),"\n",(0,r.jsxs)(s.p,{children:["Use ",(0,r.jsx)(s.code,{children:"?safe=1"})," to prioritize WebGL1 and the lowest-cost path. If it still cannot start,\ninspect ",(0,r.jsx)(s.code,{children:"chrome://gpu"})," or the Graphics section in ",(0,r.jsx)(s.code,{children:"about:support"}),", enable hardware\nacceleration, and update Mesa/the graphics driver through your distribution. A web page\ncannot force a driver after the browser refuses to create even a WebGL1 context."]}),"\n",(0,r.jsx)(s.h3,{id:"alternative-without-astro",children:"Alternative without Astro (zero build dependency)"}),"\n",(0,r.jsxs)(s.p,{children:["The evaluation harness ships a 24-line static server that serves ",(0,r.jsx)(s.code,{children:"public/"})," and\nmaps ",(0,r.jsx)(s.code,{children:"/"})," to the source of the game page:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"node tools/eval/serve.mjs 8123 # http://localhost:8123\n"})}),"\n",(0,r.jsxs)(s.p,{children:["It exists exactly because ",(0,r.jsx)(s.code,{children:"src/pages/index.astro"})," is pure HTML \u2014 you can serve the\nraw file without going through Astro (",(0,r.jsx)(s.code,{children:"tools/eval/serve.mjs:15"}),")."]}),"\n",(0,r.jsx)(s.h2,{id:"the-first-hour-gotcha",children:"The gotcha that costs everyone their first hour"}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsxs)(s.strong,{children:["There is no ",(0,r.jsx)(s.code,{children:"public/index.html"}),"."]})," Serving the ",(0,r.jsx)(s.code,{children:"public/"})," folder statically gives you a\ndirectory index with ",(0,r.jsx)(s.code,{children:"eval.html"}),", ",(0,r.jsx)(s.code,{children:"mapview.html"})," and company \u2014 none of them is the game.\nThe game's HTML is ",(0,r.jsx)(s.code,{children:"src/pages/index.astro"}),", served at the ",(0,r.jsx)(s.strong,{children:"root route"})," by Astro. There\nis no ",(0,r.jsx)(s.code,{children:"/game"})," route."]}),"\n",(0,r.jsxs)(s.p,{children:["The independent confirmation is in the harness itself: ",(0,r.jsx)(s.code,{children:"tools/eval/serve.mjs:15"})," needs a\nspecial case ",(0,r.jsx)(s.code,{children:"if (p === '/')"})," that reads ",(0,r.jsx)(s.code,{children:"src/pages/index.astro"})," from disk, precisely\nbecause there is no ",(0,r.jsx)(s.code,{children:"index.html"})," in ",(0,r.jsx)(s.code,{children:"public/"})," to serve."]}),"\n",(0,r.jsx)(s.admonition,{title:"This section used to be a list of README errors",type:"note",children:(0,r.jsxs)(s.p,{children:["Until 04/08/2026 it existed because the root ",(0,r.jsx)(s.code,{children:"README.md"})," told you to run\n",(0,r.jsx)(s.code,{children:"cd public && python3 -m http.server"}),' and spoke of a "game at ',(0,r.jsx)(s.code,{children:"/game/"}),"\". Both lines\nwere fixed \u2014 today's README says the right thing. What remains is the fact itself, which\nis still the first stumbling block for anyone arriving."]})}),"\n",(0,r.jsx)(s.h2,{id:"real-repository-structure",children:"The real repository structure"}),"\n",(0,r.jsx)(s.p,{children:"Two code zones and a third zone that is the reason this doc exists (the harness):"}),"\n",(0,r.jsxs)(s.p,{children:["No counts here: the tree says ",(0,r.jsx)(s.strong,{children:"what each thing is"}),", and the numbers live in the\ngenerated table up top. Mixing the two is how the hand-written ",(0,r.jsx)(s.code,{children:"ARCH.md"})," was born wrong."]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{children:'public/ THE GAME \u2014 vanilla ES modules, ZERO build\n js/\n game.js the Game class (loop, bots, shooting, HUD) \u2014 the largest file in the repo\n main.js menu, DOM wiring, persistence\n vmattach.js springs.js weapons.js fparms.js handik.js viewmodel/weapons\n maps.js the map REGISTRY (what is not here is not playable)\n map_brasilia.js map_piscina.js map_havan.js\n map_ferrovelho.js map_quebrada.js the registered maps\n map_piscinao_ramos.js "Piscin\xe3o" \u2014 exists on disk, OUTSIDE the registry\n mapprops.js map_decals.js props and graffiti\n bloom.js textures.js vao.js stylize.js gpuparticles.js graphics/FX\n characters.js glbchars.js characters\n audio.js version.js site-bg.js\n models/ weapons, characters, props and animation clips in GLB\n vendor/ vendored Three.js (no CDN, no npm at runtime)\n style.css the entire HUD\n *.html visual harnesses (eval, mapview, weapontest, vm-inspect\u2026)\n\nsrc/ THE SITE (Astro + Vercel adapter)\n pages/index.astro \u26a0 THIS IS THE GAME (HTML + import map + HUD)\n pages/sobre.astro landing/FAQ with JSON-LD\n pages/personagens.astro como-jogar.astro ranking.astro mapa.astro\n pages/u/[...path].astro public profile\n pages/api/*.ts SSR: leaderboard, submit-match, register, badge, avatar\n layouts/Layout.astro the site shell (not the game\'s)\n lib/ supabase, svg, geo, fmt\n\ntools/\n eval/ THE HARNESS \u2014 rulers, gate and probes. See "Quality gates"\n invariants.mjs the gate\n ref-measure.py measures the reference frames (the house doctrine)\n harness.mjs boots the real Game in node with stubbed DOM\n ARCH.md BAR.md conflict map (generated) and the visual ruler\n gen-arch.mjs generates and VALIDATES ARCH.md\n gen-docs.mjs generates and VALIDATES the numeric blocks of this documentation\n gen-asset.mjs generates a 3D prop from text (Tripo/Meshy)\n gen-image.mjs generates 2D art from text (OpenRouter)\n\n (database: schema/migrations are PRIVATE \u2014 outside the repo)\n.github/workflows/ci.yml the gate running in CI\n'})}),"\n",(0,r.jsx)(s.p,{children:"The maps registered today, and which mode each one opens in:"}),"\n","\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Id"}),(0,r.jsx)(s.th,{children:"Menu name"}),(0,r.jsx)(s.th,{children:"Opens in"}),(0,r.jsxs)(s.th,{children:["File in ",(0,r.jsx)(s.code,{children:"public/js/"})]}),(0,r.jsx)(s.th,{style:{textAlign:"right"},children:"Lines"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"praca_poderes"})}),(0,r.jsx)(s.td,{children:"Pra\xe7a dos Tr\xeas Poderes"}),(0,r.jsx)(s.td,{children:"rounds"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_brasilia.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"1,830"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"piscina_treta"})}),(0,r.jsx)(s.td,{children:"Piscina da Treta"}),(0,r.jsx)(s.td,{children:"rounds"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_piscina.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"810"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"loja_h"})}),(0,r.jsx)(s.td,{children:"Loja H (Estacionamento)"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_havan.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"1,964"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"ferro_velho"})}),(0,r.jsx)(s.td,{children:"Ferro Velho do Z\xe9"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_ferrovelho.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"1,888"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"quebrada"})}),(0,r.jsx)(s.td,{children:"Quebrada (Rua do Baile)"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_quebrada.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"1,599"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"posto_treta"})}),(0,r.jsx)(s.td,{children:"Posto da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_posto.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"489"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"upa_24h"})}),(0,r.jsx)(s.td,{children:"UPA 24h da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_upa.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"288"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"obras_prefeitura"})}),(0,r.jsx)(s.td,{children:"Obras da Prefeitura"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_obras.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"240"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"atacadao_treta"})}),(0,r.jsx)(s.td,{children:"Atacad\xe3o da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_atacadao.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"255"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"parque_treta"})}),(0,r.jsx)(s.td,{children:"Parque da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_parque.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"402"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"velho_oeste"})}),(0,r.jsx)(s.td,{children:"Velho Oeste da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_velho_oeste.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"433"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"penitenciaria"})}),(0,r.jsx)(s.td,{children:"Penitenci\xe1ria da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_penitenciaria.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"247"})]})]})]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"12 registered maps"})," - 2 open in rounds and 10 in capture. ",(0,r.jsx)(s.code,{children:"ctfMode"})," sets the initial mode; it does not lock it. There are 14 ",(0,r.jsx)(s.code,{children:"map_*.js"})," files on disk, so a file alone does ",(0,r.jsx)(s.strong,{children:"not"})," make a map playable."]}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Block generated by ",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(s.code,{children:"objeto MAPS de public/js/maps.js"})]}),"\n"]}),"\n","\n",(0,r.jsx)(s.h3,{id:"the-two-zones",children:"The two zones"}),"\n",(0,r.jsxs)(s.p,{children:["In one line each: ",(0,r.jsxs)(s.strong,{children:[(0,r.jsx)(s.code,{children:"public/"})," is the game"]})," (vanilla, ES modules, no framework and no\nbundler) and ",(0,r.jsxs)(s.strong,{children:[(0,r.jsx)(s.code,{children:"src/"})," is the site"]})," (Astro with SSR, where frameworks are welcome). What\neach boundary rule pays for, and why it is hard, is in\n",(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/stack#the-two-zones",children:"Stack and tooling"})})," \u2014 a\nsingle page, so that there are no two versions of the same boundary."]}),"\n",(0,r.jsxs)(s.p,{children:["What you need to know ",(0,r.jsx)(s.strong,{children:"before editing"})," is the consequence: the game is loaded by the\nAstro page through an ",(0,r.jsx)(s.strong,{children:"import map versioned by content hash"})," (",(0,r.jsx)(s.code,{children:"src/pages/index.astro"}),")."]}),"\n",(0,r.jsx)(s.admonition,{title:"Preserve the published manifest",type:"danger",children:(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"scripts/module-cache.mjs"})," hashes the published modules under ",(0,r.jsx)(s.code,{children:"public/js/"}),", and the import map\napplies that revision to the entire graph. Do not bump it manually or include benches\nremoved by ",(0,r.jsx)(s.code,{children:"scripts/prune-dist.mjs"}),". ",(0,r.jsx)(s.code,{children:"npm run eval:shaderbudget"})," (SB7) checks both properties."]})}),"\n",(0,r.jsx)(s.h2,{id:"commands-you-will-use",children:"Commands you will use"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"npm run dev # site + game (Astro, :4321) \u2014 the / route IS the game\nnpm run build # dist/client + dist/server\nnpm run eval:vm # viewmodel framing \u2014 RUN BEFORE the invariants\nnpm run eval:invariants # the invariants \u2014 pure node, 10-12 min\nnpm run eval:bots # botsim 60 s per map, fixed seeds\nnpm run eval:mat # material/light/fog/texture on the maps\nnpm run docs # regenerates the numeric blocks of this documentation\nnode tools/eval/serve.mjs 8123 # static server without Astro\n"})}),"\n",(0,r.jsxs)(s.p,{children:["And the two gates, with the exact list of what each one runs \u2014 straight from ",(0,r.jsx)(s.code,{children:"package.json"}),":"]}),"\n","\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:preload eval:docsautoria\n"})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"package.json"})," has ",(0,r.jsx)(s.strong,{children:"117 scripts"}),"; the reason behind each one lives in ",(0,r.jsx)(s.code,{children:"SCRIPTS.md"}),"."]}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Block generated by ",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(s.code,{children:"node -p \"Object.keys(require('./package.json').scripts)\""})]}),"\n"]}),"\n","\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"npm run check"})," is the same set the CI runs in ",(0,r.jsx)(s.code,{children:".github/workflows/ci.yml"}),"."]}),"\n",(0,r.jsxs)(s.admonition,{type:"tip",children:[(0,r.jsxs)(s.mdxAdmonitionTitle,{children:["Use ",(0,r.jsx)(s.code,{children:"check:fast"})," in the loop, ",(0,r.jsx)(s.code,{children:"check"})," before the PR"]}),(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"check"})," takes 10-12 min because it boots the game five times. ",(0,r.jsx)(s.code,{children:"check:fast"})," covers the\nrulers (quality gates) that were born from the most recent bugs (pause menu, capture\nround, regeneration, animation manifest) and runs in about a minute."]})]}),"\n",(0,r.jsx)(s.h2,{id:"where-to-go-now",children:"Where to go now"}),"\n",(0,r.jsxs)(s.p,{children:["The sidebar order ",(0,r.jsx)(s.strong,{children:"is"})," the reading order, and each page delivers one thing:"]}),"\n",(0,r.jsxs)(s.ol,{children:["\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/stack",children:"Stack and tooling"})})," \u2014 what this is built with, with the declared version\nof each piece. It is where the ",(0,r.jsx)(s.code,{children:"public/"})," \xd7 ",(0,r.jsx)(s.code,{children:"src/"})," boundary is explained in full."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/ai-instrumentation",children:"AI instrumentation"})})," \u2014 how the work gets done here. If\nyou have never collaborated with agents in a repository, start with this one."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/quality-gates",children:"The gate"})})," \u2014 what an invariant is, how to write one, the\ntwo house laws, and the mutation test of the ruler itself. ",(0,r.jsx)(s.strong,{children:"It is the most useful page\non the site."})]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/architecture",children:"Architecture"})})," \u2014 how N agents edit the same file without\ncolliding, and the conflict table. Read it before touching ",(0,r.jsx)(s.code,{children:"game.js"}),"."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/contributing",children:"How to contribute"})})," \u2014 what a PR needs to get in, and the\n",(0,r.jsx)(s.strong,{children:"first-contribution tasks"})," already written in\n",(0,r.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/tree/main/docs/issues",children:(0,r.jsx)(s.code,{children:"docs/issues/"})})," (with a\nready-made ",(0,r.jsx)(s.code,{children:"abrir-issues.sh"})," \u2014 they have not been opened on GitHub yet)."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:"License"})," \u2014 the repo-root ",(0,r.jsx)(s.code,{children:"LICENSE"})," declares it (AGPL-3.0 today); the surfaces\nthat repeat its name and must change together are listed in ",(0,r.jsx)(s.code,{children:"CONTRIBUTING.md"}),"."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"./status",children:"Current state"})})," \u2014 live sources for production health, data coverage, and debt\nsince the last pasted measurement."]}),"\n"]}),"\n",(0,r.jsxs)(s.p,{children:["Where the project is ",(0,r.jsx)(s.strong,{children:"going"})," is not in this documentation: it is\n",(0,r.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,r.jsx)(s.code,{children:"docs/ROADMAP.md"})}),", and the\nexecutable plan is\n",(0,r.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/plans/08-RELEASE-PROFISSIONAL.md",children:(0,r.jsx)(s.code,{children:"plans/08"})}),"."]})]})}function j(e={}){const{wrapper:s}={...(0,i.R)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},8453(e,s,n){n.d(s,{R:()=>d,x:()=>c});var t=n(6540);const r={},i=t.createContext(r);function d(e){const s=t.useContext(i);return t.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function c(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),t.createElement(i.Provider,{value:s},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[571],{6932(e,s,n){n.r(s),n.d(s,{assets:()=>o,contentTitle:()=>a,default:()=>j,frontMatter:()=>c,metadata:()=>t,toc:()=>l});const t=JSON.parse('{"id":"comecando","title":"What it is, and how to run it","description":"What CORO SOLTO is, how to run it in 3 commands, and the real repository structure \u2014 checked against the code.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/comecando.md","sourceDirName":".","slug":"/","permalink":"/docs/en/","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/comecando.md","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"id":"comecando","title":"What it is, and how to run it","sidebar_label":"Getting started","sidebar_position":1,"slug":"/","description":"What CORO SOLTO is, how to run it in 3 commands, and the real repository structure \u2014 checked against the code."},"sidebar":"dev","next":{"title":"Stack and tools","permalink":"/docs/en/stack"}}');var r=n(4848),i=n(8453),d=n(6025);const c={id:"comecando",title:"What it is, and how to run it",sidebar_label:"Getting started",sidebar_position:1,slug:"/",description:"What CORO SOLTO is, how to run it in 3 commands, and the real repository structure \u2014 checked against the code."},a="What it is, and how to run it",o={},l=[{value:"Run it in 3 commands",id:"run-in-3-commands",level:2},{value:"Linux, WebGL, and compatibility mode",id:"linux-webgl-and-compatibility-mode",level:3},{value:"Alternative without Astro (zero build dependency)",id:"alternative-without-astro",level:3},{value:"The gotcha that costs everyone their first hour",id:"the-first-hour-gotcha",level:2},{value:"The real repository structure",id:"real-repository-structure",level:2},{value:"The two zones",id:"the-two-zones",level:3},{value:"Commands you will use",id:"commands-you-will-use",level:2},{value:"Where to go now",id:"where-to-go-now",level:2}];function h(e){const s={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:["\n","\n","\n",(0,r.jsx)("div",{className:"cs-hero",children:(0,r.jsx)("img",{className:"cs-hero__bird",src:(0,d.Ay)("/img/canarinho-header.webp"),alt:"CORO SOLTO: Treta Suprema \u2014 the canarinho, the game's mascot, spinning",width:"604",height:"240"})}),"\n",(0,r.jsx)(s.header,{children:(0,r.jsx)(s.h1,{id:"what-it-is-and-how-to-run-it",children:"What it is, and how to run it"})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"CORO SOLTO: Treta Suprema"})," (formerly CS BRASIL) is a browser FPS written in\nvanilla JavaScript on top of Three.js r160, in the style of Counter-Strike 1.6: rounds,\nbots, AWP, Tab scoreboard, voice radio. It runs from a link, with nothing to install."]}),"\n",(0,r.jsxs)(s.p,{children:["The numbers below ",(0,r.jsx)(s.strong,{children:"are not hand-written"}),": they are regenerated by\n",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"})," from the code, and ",(0,r.jsx)(s.code,{children:"npm run docs:check"})," (inside\n",(0,r.jsx)(s.code,{children:"check:fast"}),") fails the gate when any of them diverges from the tree. Before that,\nthis page was aging at the very first commit \u2014 see\n",(0,r.jsx)(s.a,{href:"/docs/en/architecture#generated-vs-not",children:"what is generated, and what is not"}),"."]}),"\n","\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"What"}),(0,r.jsx)(s.th,{style:{textAlign:"right"},children:"How much"}),(0,r.jsx)(s.th,{children:"Where to check"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Game code"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"32,001 lines in 44 files"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files public/js/*.js | xargs wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"game.js"})}),(0,r.jsxs)(s.td,{style:{textAlign:"right"},children:[(0,r.jsx)(s.strong,{children:"6,910"})," lines"]}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"wc -l public/js/game.js"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"main.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"2,698 lines"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"wc -l public/js/main.js"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Weapons with GLB"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"26"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'public/models/weapons/*.glb' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Character GLBs"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"45"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'public/models/characters/*.glb' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Props in GLB"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"108"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'public/models/props/*.glb' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Versioned animation clips"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"573"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files public/models/anims | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Playable characters"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"44, in 5 factions"}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"CHARACTERS"})," array in ",(0,r.jsx)(s.code,{children:"characters.js"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Maps in the registry"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"12"}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"MAPS"})," object in ",(0,r.jsx)(s.code,{children:"maps.js"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Visual harnesses in HTML"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"15"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'public/*.html' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Harness scripts"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"200"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Pipeline scripts"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"54"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'tools/*.mjs' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Written entry tasks"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"26"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"git ls-files 'docs/issues/[0-9]*.md' | wc -l"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Version"}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:(0,r.jsx)(s.code,{children:"2.0.0-alpha.179"})}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"public/js/version.js"})," and ",(0,r.jsx)(s.code,{children:"package.json"})," (match)"]})]})]})]}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Block generated by ",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(s.code,{children:"the command in the right column of each row"})]}),"\n"]}),"\n","\n",(0,r.jsxs)(s.p,{children:["And the match rules that move around the most, all read from the constants in\n",(0,r.jsx)(s.code,{children:"public/js/game.js"}),":"]}),"\n","\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Rule"}),(0,r.jsx)(s.th,{children:"Value"}),(0,r.jsx)(s.th,{children:"Constant"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Factions \xb7 characters"}),(0,r.jsx)(s.td,{children:"5 \xb7 44 (B 9 \xb7 C 9 \xb7 E 8 \xb7 F 9 \xb7 U 9)"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"CHARACTERS"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Maps in the menu"}),(0,r.jsxs)(s.td,{children:["12 - 2 open in rounds, ",(0,r.jsx)(s.strong,{children:"10 in capture"})]}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"MAPS"})," / ",(0,r.jsx)(s.code,{children:"ctfMode"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Respawn"}),(0,r.jsx)(s.td,{children:"2.2 s"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"RESPAWN_DELAY"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Round"}),(0,r.jsx)(s.td,{children:"99 s, 3 wins"}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ROUND_TIME"})," / ",(0,r.jsx)(s.code,{children:"ROUNDS_TO_WIN"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Capture"}),(0,r.jsxs)(s.td,{children:["target = ",(0,r.jsx)(s.strong,{children:"all flags on the map"}),", 2 rounds (480 s safety net)"]}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"capsToWin = ctfPts.length"})," / ",(0,r.jsx)(s.code,{children:"CTF_ROUNDS_TO_WIN"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Health regeneration"}),(0,r.jsx)(s.td,{children:(0,r.jsxs)(s.strong,{children:["OFF - ",(0,r.jsx)(s.code,{children:"?regen=1"})," turns it back on"]})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"REGEN"})})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:["Ranking / ",(0,r.jsx)(s.code,{children:"/u/"})," pages"]}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"OFF - controlled by one flag"})}),(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"RANKING_ON"})," in ",(0,r.jsx)(s.code,{children:"src/lib/site.ts"})]})]})]})]}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Block generated by ",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(s.code,{children:"constantes de public/js/game.js \xb7 RANKING_ON de src/lib/site.ts"})]}),"\n"]}),"\n","\n",(0,r.jsxs)(s.p,{children:["The menu accepts from ",(0,r.jsx)(s.strong,{children:"2\xd72 to 8\xd78"})," bots (the engine accepts 1 to 8 per side); the default is 4\xd74."]}),"\n",(0,r.jsxs)(s.admonition,{title:"Two of these are a recent choice, not a defect",type:"note",children:[(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Health regeneration was turned off"})," on 05/08 (",(0,r.jsx)(s.code,{children:"REGEN = QS.get('regen') === '1'"}),"). It\nexisted, CoD-style \u2014 6 s without taking damage and 22 HP/s \u2014 and the owner reported it as\na bug (",(0,r.jsx)(s.em,{children:"\"the 1st player's health goes back to 100, I don't know why\""}),") precisely because\nit was ",(0,r.jsx)(s.strong,{children:"invisible"}),": no icon, no sound, no line in the settings. A rule the player does\nnot notice is indistinguishable from a defect. It remains fully intact behind ",(0,r.jsx)(s.code,{children:"?regen=1"}),",\nwith player\u2194bot symmetry. ",(0,r.jsx)(s.strong,{children:"Whoever turns it back on must ship the feedback along with\nit"})," \u2014 and solve what it had been papering over: with no healing, medkit, or armor, every\nlife after first contact was already lost."]}),(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"The ranking was turned off"})," and replaced with anonymous telemetry. ",(0,r.jsx)(s.code,{children:"/ranking"})," and ",(0,r.jsx)(s.code,{children:"/u/*"}),"\nrespond ",(0,r.jsxs)(s.strong,{children:["200 with a notice + ",(0,r.jsx)(s.code,{children:"noindex"})]})," (not 404 \u2014 the URLs are indexed and will come back),\nand ",(0,r.jsx)(s.code,{children:"/api/leaderboard"})," responds ",(0,r.jsx)(s.code,{children:"{disabled:true}"}),"."]})]}),"\n",(0,r.jsxs)(s.admonition,{title:"The gate is NOT green, and that is declared",type:"caution",children:[(0,r.jsxs)(s.p,{children:["How many invariants pass ",(0,r.jsx)(s.strong,{children:"is not derivable from the code"})," \u2014 it is the result of a run,\nand it even depends on which inputs exist on the machine. That is why that scoreboard is\nnot repeated here: it lives in the header of\n",(0,r.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/KNOWN-BUGS.md",children:(0,r.jsx)(s.code,{children:"KNOWN-BUGS.md"})}),", pasted\nfrom a real run, with the list of red ones, root cause, and ",(0,r.jsx)(s.code,{children:"arquivo:linha"})," for each one.\nThat is the file maintained day by day."]}),(0,r.jsx)(s.p,{children:"For today's state, run \u2014 do not repeat a number from memory:"}),(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"npm run eval:vm && node tools/eval/invariants.mjs --json # 10-12 min\n"})}),(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Order matters"}),": a viewmodel invariant measured with yesterday's JSON invents a red\n(see ",(0,r.jsx)(s.a,{href:"/docs/en/contributing#running-the-gate",children:"How to contribute"}),")."]})]}),"\n",(0,r.jsx)(s.h2,{id:"run-in-3-commands",children:"Run it in 3 commands"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"git clone https://github.com/rubenmarcus/csbrasil.git && cd csbrasil\nnpm install\nnpm run dev # opens http://localhost:4321 \u2014 this page IS the game\n"})}),"\n",(0,r.jsxs)(s.p,{children:["The audio pack (",(0,r.jsx)(s.code,{children:"npm run fetch-audio"}),") is ",(0,r.jsx)(s.strong,{children:"optional"}),": without it the game uses\nsynthesized sounds. The ",(0,r.jsx)(s.code,{children:"public/audio/"})," folder is not versioned."]}),"\n",(0,r.jsx)(s.h3,{id:"linux-webgl-and-compatibility-mode",children:"Linux, WebGL, and compatibility mode"}),"\n",(0,r.jsx)(s.p,{children:"The game tries WebGL2 and WebGL1, starting with the browser default and reducing\nantialiasing, GPU preference, and stencil before giving up. WebGL1, llvmpipe/SwiftShader,\nor another degraded tier automatically uses low quality for that session: DPR 0.75, no\nbloom or shadows, and static portraits in character selection."}),"\n",(0,r.jsxs)(s.p,{children:["Use ",(0,r.jsx)(s.code,{children:"?safe=1"})," to prioritize WebGL1 and the lowest-cost path. If it still cannot start,\ninspect ",(0,r.jsx)(s.code,{children:"chrome://gpu"})," or the Graphics section in ",(0,r.jsx)(s.code,{children:"about:support"}),", enable hardware\nacceleration, and update Mesa/the graphics driver through your distribution. A web page\ncannot force a driver after the browser refuses to create even a WebGL1 context."]}),"\n",(0,r.jsx)(s.h3,{id:"alternative-without-astro",children:"Alternative without Astro (zero build dependency)"}),"\n",(0,r.jsxs)(s.p,{children:["The evaluation harness ships a 24-line static server that serves ",(0,r.jsx)(s.code,{children:"public/"})," and\nmaps ",(0,r.jsx)(s.code,{children:"/"})," to the source of the game page:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"node tools/eval/serve.mjs 8123 # http://localhost:8123\n"})}),"\n",(0,r.jsxs)(s.p,{children:["It exists exactly because ",(0,r.jsx)(s.code,{children:"src/pages/index.astro"})," is pure HTML \u2014 you can serve the\nraw file without going through Astro (",(0,r.jsx)(s.code,{children:"tools/eval/serve.mjs:15"}),")."]}),"\n",(0,r.jsx)(s.h2,{id:"the-first-hour-gotcha",children:"The gotcha that costs everyone their first hour"}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsxs)(s.strong,{children:["There is no ",(0,r.jsx)(s.code,{children:"public/index.html"}),"."]})," Serving the ",(0,r.jsx)(s.code,{children:"public/"})," folder statically gives you a\ndirectory index with ",(0,r.jsx)(s.code,{children:"eval.html"}),", ",(0,r.jsx)(s.code,{children:"mapview.html"})," and company \u2014 none of them is the game.\nThe game's HTML is ",(0,r.jsx)(s.code,{children:"src/pages/index.astro"}),", served at the ",(0,r.jsx)(s.strong,{children:"root route"})," by Astro. There\nis no ",(0,r.jsx)(s.code,{children:"/game"})," route."]}),"\n",(0,r.jsxs)(s.p,{children:["The independent confirmation is in the harness itself: ",(0,r.jsx)(s.code,{children:"tools/eval/serve.mjs:15"})," needs a\nspecial case ",(0,r.jsx)(s.code,{children:"if (p === '/')"})," that reads ",(0,r.jsx)(s.code,{children:"src/pages/index.astro"})," from disk, precisely\nbecause there is no ",(0,r.jsx)(s.code,{children:"index.html"})," in ",(0,r.jsx)(s.code,{children:"public/"})," to serve."]}),"\n",(0,r.jsx)(s.admonition,{title:"This section used to be a list of README errors",type:"note",children:(0,r.jsxs)(s.p,{children:["Until 04/08/2026 it existed because the root ",(0,r.jsx)(s.code,{children:"README.md"})," told you to run\n",(0,r.jsx)(s.code,{children:"cd public && python3 -m http.server"}),' and spoke of a "game at ',(0,r.jsx)(s.code,{children:"/game/"}),"\". Both lines\nwere fixed \u2014 today's README says the right thing. What remains is the fact itself, which\nis still the first stumbling block for anyone arriving."]})}),"\n",(0,r.jsx)(s.h2,{id:"real-repository-structure",children:"The real repository structure"}),"\n",(0,r.jsx)(s.p,{children:"Two code zones and a third zone that is the reason this doc exists (the harness):"}),"\n",(0,r.jsxs)(s.p,{children:["No counts here: the tree says ",(0,r.jsx)(s.strong,{children:"what each thing is"}),", and the numbers live in the\ngenerated table up top. Mixing the two is how the hand-written ",(0,r.jsx)(s.code,{children:"ARCH.md"})," was born wrong."]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{children:'public/ THE GAME \u2014 vanilla ES modules, ZERO build\n js/\n game.js the Game class (loop, bots, shooting, HUD) \u2014 the largest file in the repo\n main.js menu, DOM wiring, persistence\n vmattach.js springs.js weapons.js fparms.js handik.js viewmodel/weapons\n maps.js the map REGISTRY (what is not here is not playable)\n map_brasilia.js map_piscina.js map_havan.js\n map_ferrovelho.js map_quebrada.js the registered maps\n map_piscinao_ramos.js "Piscin\xe3o" \u2014 exists on disk, OUTSIDE the registry\n mapprops.js map_decals.js props and graffiti\n bloom.js textures.js vao.js stylize.js gpuparticles.js graphics/FX\n characters.js glbchars.js characters\n audio.js version.js site-bg.js\n models/ weapons, characters, props and animation clips in GLB\n vendor/ vendored Three.js (no CDN, no npm at runtime)\n style.css the entire HUD\n *.html visual harnesses (eval, mapview, weapontest, vm-inspect\u2026)\n\nsrc/ THE SITE (Astro + Vercel adapter)\n pages/index.astro \u26a0 THIS IS THE GAME (HTML + import map + HUD)\n pages/sobre.astro landing/FAQ with JSON-LD\n pages/personagens.astro como-jogar.astro ranking.astro mapa.astro\n pages/u/[...path].astro public profile\n pages/api/*.ts SSR: leaderboard, submit-match, register, badge, avatar\n layouts/Layout.astro the site shell (not the game\'s)\n lib/ supabase, svg, geo, fmt\n\ntools/\n eval/ THE HARNESS \u2014 rulers, gate and probes. See "Quality gates"\n invariants.mjs the gate\n ref-measure.py measures the reference frames (the house doctrine)\n harness.mjs boots the real Game in node with stubbed DOM\n ARCH.md BAR.md conflict map (generated) and the visual ruler\n gen-arch.mjs generates and VALIDATES ARCH.md\n gen-docs.mjs generates and VALIDATES the numeric blocks of this documentation\n gen-asset.mjs generates a 3D prop from text (Tripo/Meshy)\n gen-image.mjs generates 2D art from text (OpenRouter)\n\n (database: schema/migrations are PRIVATE \u2014 outside the repo)\n.github/workflows/ci.yml the gate running in CI\n'})}),"\n",(0,r.jsx)(s.p,{children:"The maps registered today, and which mode each one opens in:"}),"\n","\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Id"}),(0,r.jsx)(s.th,{children:"Menu name"}),(0,r.jsx)(s.th,{children:"Opens in"}),(0,r.jsxs)(s.th,{children:["File in ",(0,r.jsx)(s.code,{children:"public/js/"})]}),(0,r.jsx)(s.th,{style:{textAlign:"right"},children:"Lines"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"praca_poderes"})}),(0,r.jsx)(s.td,{children:"Pra\xe7a dos Tr\xeas Poderes"}),(0,r.jsx)(s.td,{children:"rounds"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_brasilia.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"1,830"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"piscina_treta"})}),(0,r.jsx)(s.td,{children:"Piscina da Treta"}),(0,r.jsx)(s.td,{children:"rounds"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_piscina.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"810"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"loja_h"})}),(0,r.jsx)(s.td,{children:"Loja H (Estacionamento)"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_havan.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"1,964"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"ferro_velho"})}),(0,r.jsx)(s.td,{children:"Ferro Velho do Z\xe9"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_ferrovelho.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"1,888"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"quebrada"})}),(0,r.jsx)(s.td,{children:"Quebrada (Rua do Baile)"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_quebrada.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"1,599"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"posto_treta"})}),(0,r.jsx)(s.td,{children:"Posto da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_posto.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"489"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"upa_24h"})}),(0,r.jsx)(s.td,{children:"UPA 24h da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_upa.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"288"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"obras_prefeitura"})}),(0,r.jsx)(s.td,{children:"Obras da Prefeitura"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_obras.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"240"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"atacadao_treta"})}),(0,r.jsx)(s.td,{children:"Atacad\xe3o da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_atacadao.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"255"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"parque_treta"})}),(0,r.jsx)(s.td,{children:"Parque da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_parque.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"402"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"velho_oeste"})}),(0,r.jsx)(s.td,{children:"Velho Oeste da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_velho_oeste.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"433"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"penitenciaria"})}),(0,r.jsx)(s.td,{children:"Penitenci\xe1ria da Treta"}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.strong,{children:"capture"})}),(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"map_penitenciaria.js"})}),(0,r.jsx)(s.td,{style:{textAlign:"right"},children:"247"})]})]})]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"12 registered maps"})," - 2 open in rounds and 10 in capture. ",(0,r.jsx)(s.code,{children:"ctfMode"})," sets the initial mode; it does not lock it. There are 14 ",(0,r.jsx)(s.code,{children:"map_*.js"})," files on disk, so a file alone does ",(0,r.jsx)(s.strong,{children:"not"})," make a map playable."]}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Block generated by ",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(s.code,{children:"objeto MAPS de public/js/maps.js"})]}),"\n"]}),"\n","\n",(0,r.jsx)(s.h3,{id:"the-two-zones",children:"The two zones"}),"\n",(0,r.jsxs)(s.p,{children:["In one line each: ",(0,r.jsxs)(s.strong,{children:[(0,r.jsx)(s.code,{children:"public/"})," is the game"]})," (vanilla, ES modules, no framework and no\nbundler) and ",(0,r.jsxs)(s.strong,{children:[(0,r.jsx)(s.code,{children:"src/"})," is the site"]})," (Astro with SSR, where frameworks are welcome). What\neach boundary rule pays for, and why it is hard, is in\n",(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/stack#the-two-zones",children:"Stack and tooling"})})," \u2014 a\nsingle page, so that there are no two versions of the same boundary."]}),"\n",(0,r.jsxs)(s.p,{children:["What you need to know ",(0,r.jsx)(s.strong,{children:"before editing"})," is the consequence: the game is loaded by the\nAstro page through an ",(0,r.jsx)(s.strong,{children:"import map versioned by content hash"})," (",(0,r.jsx)(s.code,{children:"src/pages/index.astro"}),")."]}),"\n",(0,r.jsx)(s.admonition,{title:"Preserve the published manifest",type:"danger",children:(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"scripts/module-cache.mjs"})," hashes the published modules under ",(0,r.jsx)(s.code,{children:"public/js/"}),", and the import map\napplies that revision to the entire graph. Do not bump it manually or include benches\nremoved by ",(0,r.jsx)(s.code,{children:"scripts/prune-dist.mjs"}),". ",(0,r.jsx)(s.code,{children:"npm run eval:shaderbudget"})," (SB7) checks both properties."]})}),"\n",(0,r.jsx)(s.h2,{id:"commands-you-will-use",children:"Commands you will use"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"npm run dev # site + game (Astro, :4321) \u2014 the / route IS the game\nnpm run build # dist/client + dist/server\nnpm run eval:vm # viewmodel framing \u2014 RUN BEFORE the invariants\nnpm run eval:invariants # the invariants \u2014 pure node, 10-12 min\nnpm run eval:bots # botsim 60 s per map, fixed seeds\nnpm run eval:mat # material/light/fog/texture on the maps\nnpm run docs # regenerates the numeric blocks of this documentation\nnode tools/eval/serve.mjs 8123 # static server without Astro\n"})}),"\n",(0,r.jsxs)(s.p,{children:["And the two gates, with the exact list of what each one runs \u2014 straight from ",(0,r.jsx)(s.code,{children:"package.json"}),":"]}),"\n","\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam\n"})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"package.json"})," has ",(0,r.jsx)(s.strong,{children:"124 scripts"}),"; the reason behind each one lives in ",(0,r.jsx)(s.code,{children:"SCRIPTS.md"}),"."]}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Block generated by ",(0,r.jsx)(s.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(s.code,{children:"node -p \"Object.keys(require('./package.json').scripts)\""})]}),"\n"]}),"\n","\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"npm run check"})," is the same set the CI runs in ",(0,r.jsx)(s.code,{children:".github/workflows/ci.yml"}),"."]}),"\n",(0,r.jsxs)(s.admonition,{type:"tip",children:[(0,r.jsxs)(s.mdxAdmonitionTitle,{children:["Use ",(0,r.jsx)(s.code,{children:"check:fast"})," in the loop, ",(0,r.jsx)(s.code,{children:"check"})," before the PR"]}),(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"check"})," takes 10-12 min because it boots the game five times. ",(0,r.jsx)(s.code,{children:"check:fast"})," covers the\nrulers (quality gates) that were born from the most recent bugs (pause menu, capture\nround, regeneration, animation manifest) and runs in about a minute."]})]}),"\n",(0,r.jsx)(s.h2,{id:"where-to-go-now",children:"Where to go now"}),"\n",(0,r.jsxs)(s.p,{children:["The sidebar order ",(0,r.jsx)(s.strong,{children:"is"})," the reading order, and each page delivers one thing:"]}),"\n",(0,r.jsxs)(s.ol,{children:["\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/stack",children:"Stack and tooling"})})," \u2014 what this is built with, with the declared version\nof each piece. It is where the ",(0,r.jsx)(s.code,{children:"public/"})," \xd7 ",(0,r.jsx)(s.code,{children:"src/"})," boundary is explained in full."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/ai-instrumentation",children:"AI instrumentation"})})," \u2014 how the work gets done here. If\nyou have never collaborated with agents in a repository, start with this one."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/quality-gates",children:"The gate"})})," \u2014 what an invariant is, how to write one, the\ntwo house laws, and the mutation test of the ruler itself. ",(0,r.jsx)(s.strong,{children:"It is the most useful page\non the site."})]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/architecture",children:"Architecture"})})," \u2014 how N agents edit the same file without\ncolliding, and the conflict table. Read it before touching ",(0,r.jsx)(s.code,{children:"game.js"}),"."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"/docs/en/contributing",children:"How to contribute"})})," \u2014 what a PR needs to get in, and the\n",(0,r.jsx)(s.strong,{children:"first-contribution tasks"})," already written in\n",(0,r.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/tree/main/docs/issues",children:(0,r.jsx)(s.code,{children:"docs/issues/"})})," (with a\nready-made ",(0,r.jsx)(s.code,{children:"abrir-issues.sh"})," \u2014 they have not been opened on GitHub yet)."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:"License"})," \u2014 the repo-root ",(0,r.jsx)(s.code,{children:"LICENSE"})," declares it (AGPL-3.0 today); the surfaces\nthat repeat its name and must change together are listed in ",(0,r.jsx)(s.code,{children:"CONTRIBUTING.md"}),"."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.a,{href:"./status",children:"Current state"})})," \u2014 live sources for production health, data coverage, and debt\nsince the last pasted measurement."]}),"\n"]}),"\n",(0,r.jsxs)(s.p,{children:["Where the project is ",(0,r.jsx)(s.strong,{children:"going"})," is not in this documentation: it is\n",(0,r.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/docs/ROADMAP.md",children:(0,r.jsx)(s.code,{children:"docs/ROADMAP.md"})}),", and the\nexecutable plan is\n",(0,r.jsx)(s.a,{href:"https://github.com/rubenmarcus/csbrasil/blob/main/plans/08-RELEASE-PROFISSIONAL.md",children:(0,r.jsx)(s.code,{children:"plans/08"})}),"."]})]})}function j(e={}){const{wrapper:s}={...(0,i.R)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},8453(e,s,n){n.d(s,{R:()=>d,x:()=>c});var t=n(6540);const r={},i=t.createContext(r);function d(e){const s=t.useContext(i);return t.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function c(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),t.createElement(i.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/en/assets/js/dc729c33.12b1db9a.js b/public/docs/en/assets/js/dc729c33.243c0d96.js similarity index 99% rename from public/docs/en/assets/js/dc729c33.12b1db9a.js rename to public/docs/en/assets/js/dc729c33.243c0d96.js index ed9d4c781..e243ef04e 100644 --- a/public/docs/en/assets/js/dc729c33.12b1db9a.js +++ b/public/docs/en/assets/js/dc729c33.243c0d96.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[305],{5185(e,n,t){t.r(n),t.d(n,{assets:()=>h,contentTitle:()=>o,default:()=>l,frontMatter:()=>a,metadata:()=>s,toc:()=>d});const s=JSON.parse('{"id":"quality-gates","title":"The gate: invariants, provenance and mutation","description":"What an invariant is in this repo, how to write one, the two house laws with the real case behind each, and the mutation test of the ruler itself.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md","sourceDirName":".","slug":"/quality-gates","permalink":"/docs/en/quality-gates","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/quality-gates.md","tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"id":"quality-gates","title":"The gate: invariants, provenance and mutation","sidebar_label":"The gate (quality gates)","sidebar_position":4,"slug":"/quality-gates","description":"What an invariant is in this repo, how to write one, the two house laws with the real case behind each, and the mutation test of the ruler itself."},"sidebar":"dev","previous":{"title":"AI instrumentation","permalink":"/docs/en/ai-instrumentation"},"next":{"title":"BotBrain","permalink":"/docs/en/botbrain"}}');var i=t(4848),r=t(8453);const a={id:"quality-gates",title:"The gate: invariants, provenance and mutation",sidebar_label:"The gate (quality gates)",sidebar_position:4,slug:"/quality-gates",description:"What an invariant is in this repo, how to write one, the two house laws with the real case behind each, and the mutation test of the ruler itself."},o="The gate: invariants, provenance and mutation",h={},d=[{value:"Why it exists",id:"why-it-exists",level:2},{value:"What an invariant is here",id:"what-an-invariant-is-here",level:2},{value:"Severity",id:"severity",level:3},{value:"The two house laws",id:"the-two-house-laws",level:2},{value:"Law 1 \u2014 Intention that doesn't become an invariant is optimized away",id:"law-1",level:3},{value:"Law 2 \u2014 A ceiling without provenance is an opinion",id:"law-2",level:3},{value:"Mutation test of the ruler itself",id:"mutation-test",level:2},{value:"The case: 20/22 green with the fix removed",id:"the-case-20-22-green",level:3},{value:"It was not an isolated case \u2014 there were three",id:"not-an-isolated-case",level:3},{value:"Mutation as a first-class thing: ui-check.mjs",id:"mutation-first-class",level:3},{value:"How to write an invariant",id:"how-to-write-an-invariant",level:2},{value:"Anti-patterns that have already cost dearly here",id:"anti-patterns",level:3},{value:"This page is the doctrine. The step-by-step is a skill",id:"doctrine-vs-skill",level:2},{value:"Running the gate",id:"running-the-gate",level:2}];function c(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:["\n",(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"the-gate-invariants-provenance-and-mutation",children:"The gate: invariants, provenance and mutation"})}),"\n",(0,i.jsxs)(n.p,{children:["The gate of this repository is a file: ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs"}),". It runs in pure node\nand exits with code 1 if any ",(0,i.jsx)(n.strong,{children:"critical"})," invariant fails. It is what CI executes on every\nPR (",(0,i.jsx)(n.code,{children:".github/workflows/ci.yml"}),")."]}),"\n","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs"}),": ",(0,i.jsx)(n.strong,{children:"2,275 lines"}),", ",(0,i.jsx)(n.strong,{children:"65 declared invariant identifiers"}),", with ",(0,i.jsx)(n.strong,{children:"28"})," declared ",(0,i.jsx)(n.code,{children:"skip()"})," paths."]}),"\n",(0,i.jsxs)(n.li,{children:["The harness contains ",(0,i.jsx)(n.strong,{children:"192 scripts"})," in ",(0,i.jsx)(n.code,{children:"tools/eval/"}),", plus ",(0,i.jsx)(n.strong,{children:"54 pipeline scripts"})," in ",(0,i.jsx)(n.code,{children:"tools/"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["The number of critical checks in one run depends on the inputs present on that machine; dated results belong in ",(0,i.jsx)(n.code,{children:"KNOWN-BUGS.md"}),"."]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"grep -o \"put('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l\ngrep -o \"skip('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l\n"})}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Block generated by ",(0,i.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,i.jsx)(n.code,{children:"grep -o \"put('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l"})]}),"\n"]}),"\n","\n",(0,i.jsxs)(n.p,{children:["The third item above is the distinction that most confuses newcomers: ",(0,i.jsx)(n.strong,{children:"declared\nidentifier \u2260 evaluated invariant."})," Several become ",(0,i.jsx)(n.code,{children:"skip"})," instead of ",(0,i.jsx)(n.code,{children:"put"})," when their\ninput is missing (the viewmodel auditor's JSON, a GLB, a folder of anims). ",(0,i.jsx)(n.code,{children:"skip"})," is a\n",(0,i.jsx)(n.strong,{children:"green gate by absence of data"}),', and that is why it always carries the reason. See\n"Severity", below.']}),"\n",(0,i.jsx)(n.p,{children:"This page is the most useful one on the site. If you only read one, read this one."}),"\n",(0,i.jsx)(n.h2,{id:"why-it-exists",children:"Why it exists"}),"\n",(0,i.jsxs)(n.p,{children:["From the header of the file itself, ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:5-19"}),":"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["The owner spent 3 days in a cycle where every round fixed one thing and broke\nanother, and we only found out one round later. The cause wasn't lack of care:\nit was the lack of a RULER (quality gate). A critic (human or agent) judges screenshots;\nconsistency and flow are properties of the game ",(0,i.jsx)(n.strong,{children:"IN MOTION"}),", and almost every defect he\nreported is not taste \u2014 it's a violated invariant."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"And the translation, which is the most important thing in this entire repository:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"What the owner said"}),(0,i.jsx)(n.th,{children:"Which invariant it became"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"the hands are floating in the air"'}),(0,i.jsx)(n.td,{children:"hand\u2194grip distance has a ceiling"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"the weapon points downward"'}),(0,i.jsx)(n.td,{children:"the barrel has a maximum angle"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"in ADS I can\'t see the weapon or the crosshair"'}),(0,i.jsx)(n.td,{children:"the weapon has a minimum and maximum area"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"sniper with no zoom"'}),(0,i.jsx)(n.td,{children:"aiming FOV < hip FOV"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"several weapons look the same"'}),(0,i.jsx)(n.td,{children:"silhouettes must differ"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"the bot shoots out of nowhere"'}),(0,i.jsx)(n.td,{children:"damage requires prior LOS"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"there are 2 of them eliminating me"'}),(0,i.jsx)(n.td,{children:"1 killfeed per death"})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:20-21"}),":"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"GOLDEN RULE: nothing gets committed with a RED invariant. And every new bug the owner\nreports becomes an invariant here \u2014 that's how it never comes back."})}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"what-an-invariant-is-here",children:"What an invariant is here"}),"\n",(0,i.jsxs)(n.p,{children:["An invariant is a ",(0,i.jsx)(n.strong,{children:"property of the game that can be measured without a human watching"}),",\nwith a ceiling or a range that has provenance. It is not a unit test: almost no invariant\ntests a function. They measure the ",(0,i.jsx)(n.strong,{children:"state of the game actually running"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Three forms, all present in the file:"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"1. Read from the source code."})," Cheap, runs in milliseconds, catches whole classes of bug.\nReal example, quoted verbatim from ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:1439-1446"})," \u2014 the file's\ncomments are written in Portuguese:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:"// ARM1 \u2014 toda arma com luneta precisa de zoom de verdade. \"Snipers sem zoom\"\n// \xe9 reclama\xe7\xe3o literal; a solu\xe7\xe3o N\xc3O \xe9 tirar a luneta, \xe9 fazer a certa.\nconst bloco = gsrc.slice(0, gsrc.indexOf('};', gsrc.indexOf('const WEAPONS')) + 2);\nconst linhas = bloco.split('\\n').filter((l) => /^\\s*\\w+:\\s*\\{/.test(l));\nconst semZoom = linhas.filter((l) => /scope:\\s*true/.test(l) && !/spreadScope/.test(l))\n .map((l) => l.trim().split(':')[0]);\nput('ARM1', 'toda arma com scope:true declara spreadScope', semZoom.length === 0,\n semZoom.length ? semZoom.join(', ') : `${linhas.length} armas conferidas`);\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"2. Measured in the real game running in node."})," ",(0,i.jsx)(n.code,{children:"tools/eval/harness.mjs"})," boots the real\n",(0,i.jsx)(n.code,{children:"Game"})," class, with the real maps, with DOM/canvas stubbed. It is the ",(0,i.jsx)(n.strong,{children:"production code"}),"\nthat gets measured, not a reimplementation \u2014 ",(0,i.jsx)(n.code,{children:"tools/eval/botsim.mjs:8-9"}),": ",(0,i.jsx)(n.em,{children:'"if the number\nimproves here, it improved in the game"'}),". This is where BOT1\u2013BOT8, MAP1\u2013MAP3, CTF1, MAT1/MAT2,\nFOG1, TEX1, VM14, MOD1/MOD2 come from."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"3. Measured on the geometry of the assets."})," ",(0,i.jsx)(n.code,{children:"vm-mint-audit.mjs"})," opens every weapon GLB with\nits own GLB parser and projects the viewmodel onto the screen. This is where VM1\u2013VM19 come from."]}),"\n",(0,i.jsxs)(n.p,{children:["What does ",(0,i.jsx)(n.strong,{children:"not"})," belong here: an invariant that requires browser pixels. Those are marked\n",(0,i.jsx)(n.code,{children:"browser"})," and are skipped, with the reason stated \u2014 SwiftShader costs ~4 min per map load\non this machine (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:99"}),")."]}),"\n",(0,i.jsx)(n.h3,{id:"severity",children:"Severity"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"put(id, desc, ok, evid, sev)"})," accepts ",(0,i.jsx)(n.code,{children:"'crit'"})," (the default) or ",(0,i.jsx)(n.code,{children:"'warn'"}),"\n(",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:81-82"}),"). A red critical fails the PR. Warn is measured noise that\nsomeone needs to look at but doesn't block \u2014 it's where BOT1/BOT2/BOT3/BOT6/BOT7, ARM4 and\nARM5 live. ",(0,i.jsx)(n.code,{children:"skip()"})," is the third state, and it is ",(0,i.jsx)(n.strong,{children:"dangerous"}),": a green gate by absence of\ndata. That is why every ",(0,i.jsx)(n.code,{children:"skip"})," carries the reason."]}),"\n",(0,i.jsx)(n.h2,{id:"the-two-house-laws",children:"The two house laws"}),"\n",(0,i.jsx)(n.h3,{id:"law-1",children:"Law 1 \u2014 Intention that doesn't become an invariant is optimized away"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsxs)(n.strong,{children:["Source: ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:452-461"}),"."]})," The case, verbatim:"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["the previous round took the gate from ",(0,i.jsx)(n.strong,{children:"16/21 to 19/21 without loosening a single ceiling"})," and\nwas still ",(0,i.jsx)(n.strong,{children:"REJECTED"})," by the owner, because to close VM5/VM10 it ",(0,i.jsxs)(n.strong,{children:["ZEROED the\n",(0,i.jsx)(n.code,{children:"VM_OFF"})," y"]}),' and silently changed the look. No invariant encoded "where the\nmuzzle sits", so the metric was optimized and the INTENT was destroyed. Goodhart\'s\nlaw, in full. ',(0,i.jsx)(n.strong,{children:"INTENTION THAT DOESN'T BECOME AN INVARIANT IS OPTIMIZED AWAY."})]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Read again what happened, because it is counterintuitive: the agent ",(0,i.jsx)(n.strong,{children:"did not cheat"}),". It\ndidn't loosen any ceiling. It genuinely raised the score. And the result was worse, because\n",(0,i.jsx)(n.code,{children:"VM_OFF[1]"})," is the term that ",(0,i.jsx)(n.strong,{children:"dominates the weapon's position on screen"})," \u2014 ",(0,i.jsx)(n.code,{children:"public/js/game.js:555"}),"\ndeclares ",(0,i.jsx)(n.code,{children:"VM_OFF = [0.03, -0.1000, 0]"}),", and ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:1163"})," measures the\nsensitivity: ",(0,i.jsx)(n.em,{children:'"removing recuoZ moves the grip 3,5 cm; removing VM_OFF moves it 23 cm"'}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Zeroing that term closed two invariants and erased the aesthetic decision the owner had\nmade \u2014 which wasn't written anywhere the ruler could read."}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"The fix was not punishing the agent. It was writing the intention as an invariant."})," Today\nthere is VM12 (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:497"}),"): ",(0,i.jsx)(n.em,{children:'"CS 1.6 look: muzzle RIGHT below\nthe crosshair (y between 0,50 and 0,62) in the 2 aspects"'}),". With it in place, the same\noptimization goes ",(0,i.jsx)(n.strong,{children:"red"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"And the operational consequence, from the same comment:"}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Whoever wants to change the look has to change ",(0,i.jsx)(n.strong,{children:"THIS ceiling"})," explicitly, in a diff the\nowner sees, instead of touching ",(0,i.jsx)(n.code,{children:"VM_OFF"}),' and reporting "+3 invariants".']}),"\n"]}),"\n",(0,i.jsx)(n.admonition,{title:"What this means for your PR",type:"tip",children:(0,i.jsxs)(n.p,{children:["If your change improves the gate's score, the first question is: ",(0,i.jsx)(n.strong,{children:"what did I change that\nthe gate doesn't look at?"}),' If the answer is "the look", "the feel" or "the vibe", write the\ninvariant before sending the PR \u2014 or explain in the PR why it doesn\'t fit.']})}),"\n",(0,i.jsx)(n.h3,{id:"law-2",children:"Law 2 \u2014 A ceiling without provenance is an opinion"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsxs)(n.strong,{children:["Source: ",(0,i.jsx)(n.code,{children:"tools/eval/ref-measure.py:1-40"}),"."]})," That docstring is the house doctrine. The case:"]}),"\n",(0,i.jsxs)(n.p,{children:["For ",(0,i.jsx)(n.strong,{children:"three days"})," the weapons gate was solved against ",(0,i.jsx)(n.strong,{children:"asserted"})," numbers:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["VM12 required ",(0,i.jsx)(n.em,{children:'"muzzle at y \u2265 0,66"'}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"vmattach.js"})," doc said ",(0,i.jsx)(n.em,{children:'"ENTIRE stock in the corner"'}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Neither of the two was measured on any image. According to ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:461-463"}),",\nthe 0,66 floor came from a comment in ",(0,i.jsx)(n.code,{children:"public/js/vmattach.js"})," \u2014 ",(0,i.jsx)(n.em,{children:'"the muzzle sits at ~0,66H"'})," \u2014\nwhich in turn came from a video someone watched. (The gate's comment points to\n",(0,i.jsx)(n.code,{children:"vmattach.js:387-392"}),"; today the text is at ",(0,i.jsx)(n.code,{children:"vmattach.js:395"}),", because the file moved. That is\nexactly why ",(0,i.jsx)(n.code,{children:"ARCH.md"})," is generated \u2014 see ",(0,i.jsx)(n.a,{href:"/docs/en/architecture",children:"Architecture"}),".)"]}),"\n",(0,i.jsxs)(n.p,{children:["The owner looked at the result and said, verbatim (",(0,i.jsx)(n.code,{children:"ref-measure.py:14-17"}),"):"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:'"it looks different from CS 1.6 and Quake and UT; in those 3 the weapon is always in the\nbottom-right corner and the stock is always OUTSIDE; after 3 days and an entire folder of\nreference neither you nor Kimi understood that."'}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Then the frames were ",(0,i.jsx)(n.strong,{children:"measured"}),". ",(0,i.jsx)(n.code,{children:"tools/eval/ref-measure.py"})," does color segmentation on the\nbottom-right quadrant, takes the largest connected component, and writes\n",(0,i.jsx)(n.code,{children:"tools/eval/ref_viewmodel.json"}),". Result:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Frame"}),(0,i.jsx)(n.th,{children:"Muzzle (x, y)"}),(0,i.jsx)(n.th,{style:{textAlign:"right"},children:"Screen area"}),(0,i.jsx)(n.th,{style:{textAlign:"right"},children:"Axis angle"}),(0,i.jsx)(n.th,{children:"Crosses the right edge?"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"cs16_ak_dust.jpg"})}),(0,i.jsxs)(n.td,{children:["0,564 ; ",(0,i.jsx)(n.strong,{children:"0,513"})]}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"9,76%"}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"28,0\xb0"}),(0,i.jsx)(n.td,{children:"yes"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"cs16_m4_dust.jpg"})}),(0,i.jsxs)(n.td,{children:["0,569 ; ",(0,i.jsx)(n.strong,{children:"0,598"})]}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"9,78%"}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"34,8\xb0"}),(0,i.jsx)(n.td,{children:"yes"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"valorant_vandal.jpg"})}),(0,i.jsxs)(n.td,{children:["0,648 ; ",(0,i.jsx)(n.strong,{children:"0,587"})]}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"13,09%"}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"4,6\xb0"}),(0,i.jsx)(n.td,{children:"yes"})]})]})]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Both asserted numbers were wrong:"})}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["The CS 1.6 muzzle sits at ",(0,i.jsx)(n.strong,{children:"0,513\u20130,598"})," \u2014 right below the crosshair (0,5), 1 to 10\npercentage points below center. Not at 0,66\u20130,93. The wrong floor was keeping our\nweapon ",(0,i.jsx)(n.strong,{children:"sunk"})," at 0,667\u20130,816 (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:472-475"}),")."]}),"\n",(0,i.jsxs)(n.li,{children:["The stock ",(0,i.jsx)(n.strong,{children:"EXITS through the corner"})," in all 3 frames. Exiting is the standard, not the defect\n(",(0,i.jsx)(n.code,{children:"ref_viewmodel.json"})," \u2192 ",(0,i.jsx)(n.code,{children:"faixas.cruzaBordaDireita: true"})," in all 3)."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["And the collateral damage: with the false ceiling, the previous round's solver ",(0,i.jsx)(n.em,{children:'"proved"'})," that 3%\nof area was infeasible. The proof was correct ",(0,i.jsx)(n.strong,{children:"against that ceiling"})," \u2014 and it was the ceiling that\nwas false (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:476-478"}),")."]}),"\n",(0,i.jsxs)(n.p,{children:["The rule that remained, ",(0,i.jsx)(n.code,{children:"tools/eval/ref-measure.py:21-22"}),":"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"AN INVARIANT CEILING ONLY GETS IN WITH PROVENANCE \u2014 reference file, measured pixel, and\nthis script reproducing the number. A number without an image is an opinion."})}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Today the framing invariants carry the provenance in their own text: VM1 (range\n0,50\u20130,60, ref 0,520\u20130,565), VM3 (22\u201342\xb0, ref 28,0\xb0 and 34,8\xb0), VM5 (6\u201316%, ref\n9,76\u201313,09%), VM12 (0,50\u20130,62, ref 0,513\u20130,598), VM16 (slice at the right edge 0,02\u20130,20,\nref 0,053\u20130,095)."}),"\n",(0,i.jsx)(n.admonition,{title:"Provenance includes admitting what the image does NOT measure",type:"note",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:599-602"}),' refuses to create a ceiling for "how much of the weapon\nsits outside the frame", because what is outside is invisible in the photo \u2014 there is no way to\nknow whether the AK\'s stock ends 5 cm or 50 cm past the edge. The numbers remain in the JSON as\n',(0,i.jsx)(n.strong,{children:"evidence, without a gate"}),". That is provenance taken seriously: the ruler says where it stops\nknowing."]})}),"\n",(0,i.jsxs)(n.p,{children:["And the same rigor bites whoever wrote the ruler, in the most uncomfortable case possible: ",(0,i.jsx)(n.strong,{children:"the\ncharacter reference photos arrived, were measured, and were REJECTED by the ruler\nitself."})," ",(0,i.jsx)(n.code,{children:"tools/eval/char-probe.mjs:25-45"})," tells the whole episode \u2014 ",(0,i.jsx)(n.code,{children:"references/funkeiros/"}),"\nhas 23 files and ",(0,i.jsx)(n.code,{children:"references/palhacos/"})," has 21, all run through ",(0,i.jsx)(n.code,{children:"ref-body.py"}),", with the\nmasks ",(0,i.jsx)(n.strong,{children:"looked at"})," (",(0,i.jsx)(n.code,{children:"--masks"}),"). The verdict, said to our face by the comment itself: they are\nselfies and close-ups; the heuristic segmentation returns the hand, a piece of jacket or the hair\nof someone else in the background, and the shoulder/height ratio comes out between ",(0,i.jsx)(n.strong,{children:"0,42 and 3,78"})," when a human\nmeasures 0,259. About ~1 usable full-body photo remains \u2014 that is not a sample."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"ref-body.py"})," requires ",(0,i.jsx)(n.strong,{children:"6 accepted photos"})," for a ceiling to become measured, and it ",(0,i.jsx)(n.strong,{children:"says why\nit didn't"}),". So the absolute ceiling of CHR1 remains a ",(0,i.jsx)(n.strong,{children:"published fallback"})," (Drillis &\nContini 1966, via Winter), declared as such in the ",(0,i.jsx)(n.code,{children:"procedencia"})," field of the JSON and in the\nreport column."]}),"\n",(0,i.jsxs)(n.p,{children:["Notice what that means: having the photo is ",(0,i.jsx)(n.strong,{children:"not"})," having the measurement. It was easier to accept\nthat the data was bad than to promote a fragile measurement to a ceiling \u2014 and that is Law 2\napplied against the interest of whoever wrote the ruler."]}),"\n",(0,i.jsxs)(n.admonition,{type:"warning",children:[(0,i.jsxs)(n.mdxAdmonitionTitle,{children:[(0,i.jsx)(n.code,{children:"references/"})," does NOT come with the clone \u2014 and that is a decision, not carelessness"]}),(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"git ls-files references"})," returns ",(0,i.jsx)(n.strong,{children:"zero"}),". On 04/08/2026 the entire folder was\nuntracked by the owner's decision (\"references can stay local because we're going to build\nlocally\"): they are the UI target screens and the viewmodel reference frames, and they live only on\nhis machine."]}),(0,i.jsxs)(n.p,{children:["What ",(0,i.jsx)(n.strong,{children:"survives the clone are the NUMBERS measured from them"}),": ",(0,i.jsx)(n.code,{children:"tools/eval/ref_ui.json"})," and\n",(0,i.jsx)(n.code,{children:"tools/eval/ref_viewmodel.json"})," are versioned. That is the contract \u2014 if a ruler of yours\nneeds to run in CI, it reads the JSON, never the PNG. A ruler that opens an image from\n",(0,i.jsx)(n.code,{children:"references/"})," goes red on every machine that isn't the owner's, and red-by-environment is\nthe worst kind: it teaches whoever works here to ignore red."]})]}),"\n",(0,i.jsx)(n.h2,{id:"mutation-test",children:"Mutation test of the ruler itself"}),"\n",(0,i.jsx)(n.p,{children:"This is the part almost no project has, and it is where this repository is genuinely\ndifferent."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"A gate that doesn't move when you break the code on purpose is blind."})}),"\n",(0,i.jsxs)(n.p,{children:["The way to find that out is to mutate: take the fixed code, ",(0,i.jsx)(n.strong,{children:"undo the fix on\npurpose"}),", run the gate, and see whether it goes red. If it stays green, the gate is not\nmeasuring what you think it measures."]}),"\n",(0,i.jsx)(n.h3,{id:"the-case-20-22-green",children:"The case: 20/22 green with the fix removed"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsxs)(n.strong,{children:["Source: ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:910-920"}),"."]})}),"\n",(0,i.jsxs)(n.p,{children:["The context: ",(0,i.jsx)(n.code,{children:"public/js/game.js:577"})," declares"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:"const vmOffY = (aspect) => VM_OFF[1] * ((16 / 9) / (aspect || 16 / 9));\n"})}),"\n",(0,i.jsxs)(n.p,{children:["It is the per-aspect vertical framing fix \u2014 the reason the weapon sits in the same\nplace in 16:9 and in 3:2 (the owner plays in 3:2). It is ",(0,i.jsx)(n.strong,{children:"called"})," in the Y argument of\n",(0,i.jsx)(n.code,{children:"this.vm.root.position.set(...)"}),", at ",(0,i.jsx)(n.code,{children:"public/js/game.js:4873"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"The hole, measured in 08/2026:"}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["the ",(0,i.jsx)(n.code,{children:"vmOff"})," step only checked ",(0,i.jsx)(n.code,{children:"/this\\.vm\\.root\\.position\\.set\\(\\s*VM_OFF\\[0\\]/"})," \u2014 the X\nterm. The Y term was checked by no one, and the auditor (",(0,i.jsx)(n.code,{children:"vm-mint-audit.mjs:196"}),",\n",(0,i.jsx)(n.code,{children:"loadOffYFn"}),") reads the ",(0,i.jsx)(n.strong,{children:"DECLARATION"})," ",(0,i.jsx)(n.code,{children:"const vmOffY = (aspect) => ..."})," by regex ",(0,i.jsx)(n.strong,{children:"without ever\nasking whether anyone CALLS it"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Result: swapping in ",(0,i.jsx)(n.code,{children:"game.js"})," the call ",(0,i.jsx)(n.code,{children:"vmOffY(...)"})," for ",(0,i.jsx)(n.code,{children:"VM_OFF[1]"})," in the Y argument\n\u2014 that is, ",(0,i.jsx)(n.strong,{children:"removing the per-aspect vertical framing fix entirely"})," \u2014 the\nwhole gate stayed ",(0,i.jsx)(n.strong,{children:"GREEN (20/22, with VM9, VM10, VM12 and VM15 all green)"}),". A\ngate that cannot tell the fixed build from the build without the fix is measuring\nnothing."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Notice the mechanism of the error, because it repeats in any language: ",(0,i.jsxs)(n.strong,{children:["the invariant\nwas reading the ",(0,i.jsx)(n.em,{children:"declaration"})," of a constant, and not the ",(0,i.jsx)(n.em,{children:"use"}),"."]})," Declaring and not calling is the\ncheapest way for a fix to vanish with the gate green."]}),"\n",(0,i.jsxs)(n.p,{children:["The repair was surgical and worth copying. AUD1 today separates the three arguments of\n",(0,i.jsx)(n.code,{children:"position.set(...)"})," with a ",(0,i.jsx)(n.strong,{children:"parenthesis scanner"})," \u2014 not ",(0,i.jsx)(n.code,{children:"split(',')"}),", which would cut\ninside the function call \u2014 and requires ",(0,i.jsx)(n.strong,{children:"by name"})," that the Y argument calls ",(0,i.jsx)(n.code,{children:"vmOffY("}),".\nAnd it closes the other path along with it (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:1148-1151"}),"): the formula of\n",(0,i.jsx)(n.code,{children:"vmOffY"})," is ",(0,i.jsxs)(n.strong,{children:["read from ",(0,i.jsx)(n.code,{children:"game.js"})," and evaluated"]})," at 16/9, and it has to yield exactly ",(0,i.jsx)(n.code,{children:"VM_OFF[1]"}),"."]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["The two checks together cover the two ways for the fix to vanish: ",(0,i.jsx)(n.strong,{children:"deleting the CALL"}),"\n(mutation measured in 08/2026) or ",(0,i.jsx)(n.strong,{children:"tampering with the FORMULA"}),"."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"not-an-isolated-case",children:"It was not an isolated case \u2014 there were three"}),"\n",(0,i.jsx)(n.p,{children:"The same hole showed up in two other places, and each one became a new AUD1 step:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Mutation"}),(0,i.jsx)(n.th,{children:"Score with the fix undone"}),(0,i.jsx)(n.th,{children:"Cause of the false green"}),(0,i.jsx)(n.th,{children:"Where"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["Swap ",(0,i.jsx)(n.code,{children:"vmOffY(...)"})," for ",(0,i.jsx)(n.code,{children:"VM_OFF[1]"})," in the Y argument"]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"20/22 green"})}),(0,i.jsx)(n.td,{children:"the invariant read the declaration, not the use"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"invariants.mjs:910-920"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["Swap ",(0,i.jsx)(n.code,{children:"g.rotation.set(pit, yaw, t.roll)"})," for ",(0,i.jsx)(n.code,{children:"g.rotation.set(0, 0, t.roll)"})]}),(0,i.jsx)(n.td,{children:"green"}),(0,i.jsxs)(n.td,{children:["the ",(0,i.jsx)(n.code,{children:"VM_FRAME.cls"})," table still holds the angles, and the three mirrors still match ",(0,i.jsx)(n.strong,{children:"each other"})]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"invariants.mjs:932-944"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["Delete ",(0,i.jsx)(n.code,{children:"* (weaponCFG(id).vm ?? 1)"})," from the mesh scale"]}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.strong,{children:"28/37 green, AUD1 included"}),' ("worst \u0394scale 0.0004")']}),(0,i.jsxs)(n.td,{children:["both ends read ",(0,i.jsx)(n.code,{children:"vm"})," from ",(0,i.jsx)(n.code,{children:"weapons.js"}),"; ",(0,i.jsxs)(n.strong,{children:[(0,i.jsx)(n.code,{children:"game.js"})," is never asked"]})," \u2014 it was the auditor checking itself"]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"invariants.mjs:971-975"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["Mutate ",(0,i.jsx)(n.code,{children:"this._adsPose['pistol']"})]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"20/22 green"})}),(0,i.jsx)(n.td,{children:"ADS had no invariant at all"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"invariants.mjs:1185"})})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"The common pattern of the four is the same, and it is what you should look for in your invariant:"}),"\n",(0,i.jsx)(n.admonition,{title:"The false-green pattern",type:"danger",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"The ruler is checking a copy of the rule instead of the game."})," Whether because it reads the declaration\nand not the use, or because it compares two mirrors that read the same source, or because the\nparameter table stays correct while no one applies it. If the two ends of your\ncomparison can stay consistent ",(0,i.jsx)(n.strong,{children:"without going through the production code"}),", your\ninvariant is blind."]})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"tools/eval/mat-check.mjs:18-27"})," solves this in the most direct way possible: the body of\n",(0,i.jsx)(n.code,{children:"fixVmMaterials"})," is ",(0,i.jsxs)(n.strong,{children:["cut out of ",(0,i.jsx)(n.code,{children:"game.js"})," and executed"]})," against a probe material. If the\ncode changes, the ruler changes with it. ",(0,i.jsx)(n.em,{children:'"a ruler that carries a COPY of the rule lies on the day\nthe rule changes."'})]}),"\n",(0,i.jsxs)(n.h3,{id:"mutation-first-class",children:["Mutation as a first-class thing: ",(0,i.jsx)(n.code,{children:"ui-check.mjs"})]}),"\n",(0,i.jsxs)(n.p,{children:["The UI harness has a ",(0,i.jsx)(n.strong,{children:"versioned mutation table"}),", and each one declares which gate\nhas to go red. ",(0,i.jsx)(n.code,{children:"tools/eval/ui-check.mjs:1046-1050"}),":"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Each mutation UNDOES one of this round's fixes (or punches through a gate on purpose) and says\nwhich gate MUST go red. ",(0,i.jsx)(n.strong,{children:"A ruler that does not fail the previous version of\nits own file is not a ruler, it is decoration."})]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Running one:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"MUT=ui1_ctf_scrim_fraco node tools/eval/ui-check.mjs # expects UI1 to go RED\nMUT=ui3_prompt_na_mira node tools/eval/ui-check.mjs # expects UI3 to go RED\nMUT=ui2_prompt_eterno node tools/eval/ui-check.mjs # expects UI2 to go RED\nMUT=ui4_ctf_sem_relogio node tools/eval/ui-check.mjs # expects UI4 to go RED\n"})}),"\n",(0,i.jsxs)(n.p,{children:["The 7 mutations are in ",(0,i.jsx)(n.code,{children:"tools/eval/ui-check.mjs:1051-1134"}),". Two mechanics: ",(0,i.jsx)(n.code,{children:"css"})," rewrites\n",(0,i.jsx)(n.code,{children:"public/style.css"})," ",(0,i.jsx)(n.strong,{children:"read in memory"})," (never on disk \u2014 other agents are editing the\nfile right now), and ",(0,i.jsx)(n.code,{children:"sim"})," monkey-patches the already-booted ",(0,i.jsx)(n.code,{children:"Game"})," object. If the ",(0,i.jsx)(n.code,{children:"css"})," mutation\nmatches nothing, the script exits with code 2 saying ",(0,i.jsx)(n.em,{children:'"the CSS changed shape"'})," \u2014 because a\nmutation that doesn't apply is also a false green (",(0,i.jsx)(n.code,{children:"ui-check.mjs:1164"}),")."]}),"\n",(0,i.jsx)(n.h2,{id:"how-to-write-an-invariant",children:"How to write an invariant"}),"\n",(0,i.jsx)(n.p,{children:"Checklist, in order:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Start from the defect's sentence."})," Verbatim, with the words of whoever complained. Every\nharness in this codebase starts that way, and it is not style: it is what keeps the invariant from\nmeasuring something else. See the header of ",(0,i.jsx)(n.code,{children:"tools/eval/map-check.mjs:5-12"})," \u2014 five sentences from the owner,\nfive invariants."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Translate it into a measurable quantity."}),' "the players are SUBMERGED UNDER THE\nSTATUE" \u2192 ',(0,i.jsx)(n.em,{children:"there is visible map geometry whose top rises more than 0,30 m above the floor\nlocal to that point"})," (MAP1). Note that the operational definition includes ",(0,i.jsx)(n.strong,{children:"why 0,30 m"}),':\nit is the step the body climbs; above that it is not "stepping over", it is "being inside".']}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Find the ceiling's provenance."})," Reference file + measured pixel + script that\nreproduces it. If it doesn't exist, ",(0,i.jsx)(n.strong,{children:"say it is a fallback"})," and cite the published source, as C1\ndoes. Never invent the number."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Measure the production code, not a copy of it."})," Import the real module, cut the\nfunction out of the file and execute it, or require the call by name in the source text."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Mutate and confirm it goes red."})," Undo the fix you just made and\nrun the gate. If it stays green, your invariant is blind \u2014 go back to step 4. If it can\nbe automated, register the mutation in a table, as ",(0,i.jsx)(n.code,{children:"ui-check.mjs"})," does."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Write the evidence, not just the boolean."})," The fourth argument of ",(0,i.jsx)(n.code,{children:"put()"})," is what\nsomeone will read three months from now: ",(0,i.jsx)(n.code,{children:'"0,504 a 0,619 da altura em 52 medidas | 0 fora da faixa"'})," is useful; ",(0,i.jsx)(n.code,{children:'"ok"'})," is not."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Write the provenance comment above it."})," In Portuguese, saying what\nhappened when the number was wrong. That comment is what keeps the next round\nfrom redoing the mistake."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"anti-patterns",children:"Anti-patterns that have already cost dearly here"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Anti-pattern"}),(0,i.jsx)(n.th,{children:"What it produced"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Reading a constant's declaration instead of its use"}),(0,i.jsx)(n.td,{children:"20/22 green with the fix removed"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Two mirrors that read the same source"}),(0,i.jsx)(n.td,{children:'28/37 green, "worst \u0394scale 0.0004", with the knob turned off'})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Format adapter silently broken"}),(0,i.jsxs)(n.td,{children:["VM1\u2013VM6 have been ",(0,i.jsx)(n.strong,{children:"SKIPPED since the auditor exists"})," \u2014 6 viewmodel invariants that never ran once (",(0,i.jsx)(n.code,{children:"invariants.mjs:121-127"}),")"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Measuring the gap against the wrong local floor"}),(0,i.jsxs)(n.td,{children:["a pickup inside the pool reported gap ",(0,i.jsx)(n.strong,{children:"0,0000 \u2014 GREEN"})," (",(0,i.jsx)(n.code,{children:"pickup-check.mjs:20-23"}),")"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"waypoint \u2264 3 m" as a reachability proxy'}),(0,i.jsxs)(n.td,{children:["74 false positives and green in a closed pocket (",(0,i.jsx)(n.code,{children:"pickup-check.mjs:34-42"}),")"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Floor without ceiling"}),(0,i.jsxs)(n.td,{children:['"muzzle \u2265 0,66" accepts the muzzle at 0,95 (weapon in the basement) \u2014 that is how we got to 0,816 (',(0,i.jsx)(n.code,{children:"invariants.mjs:432-434"}),")"]})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"doctrine-vs-skill",children:"This page is the doctrine. The step-by-step is a skill"}),"\n",(0,i.jsxs)(n.p,{children:["What to do, in order, when someone reports a defect \u2014 reproduce, measure before\nfixing, refute the obvious guess, mutate the ruler, run the gate in the right order and report\nwhat was ",(0,i.jsx)(n.strong,{children:"not"})," verified \u2014 is in ",(0,i.jsx)(n.code,{children:".claude/skills/bug-hunt/SKILL.md"}),", with the real case\nthat paid for each rule. It is written for agents ",(0,i.jsx)(n.strong,{children:"and"})," for people, and it points back to\nthis page instead of repeating it."]}),"\n",(0,i.jsx)(n.h2,{id:"running-the-gate",children:"Running the gate"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"node tools/eval/invariants.mjs # everything that runs without a browser\nnode tools/eval/invariants.mjs --json # machine-readable output\nnpm run check # syntax + gate + vm + recoil + bots\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Current production, data, and debt sources: ",(0,i.jsx)(n.a,{href:"./status",children:"Current state"}),"."]})]})}function l(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},8453(e,n,t){t.d(n,{R:()=>a,x:()=>o});var s=t(6540);const i={},r=s.createContext(i);function a(e){const n=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[305],{5185(e,n,t){t.r(n),t.d(n,{assets:()=>h,contentTitle:()=>o,default:()=>l,frontMatter:()=>a,metadata:()=>s,toc:()=>d});const s=JSON.parse('{"id":"quality-gates","title":"The gate: invariants, provenance and mutation","description":"What an invariant is in this repo, how to write one, the two house laws with the real case behind each, and the mutation test of the ruler itself.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md","sourceDirName":".","slug":"/quality-gates","permalink":"/docs/en/quality-gates","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/quality-gates.md","tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"id":"quality-gates","title":"The gate: invariants, provenance and mutation","sidebar_label":"The gate (quality gates)","sidebar_position":4,"slug":"/quality-gates","description":"What an invariant is in this repo, how to write one, the two house laws with the real case behind each, and the mutation test of the ruler itself."},"sidebar":"dev","previous":{"title":"AI instrumentation","permalink":"/docs/en/ai-instrumentation"},"next":{"title":"BotBrain","permalink":"/docs/en/botbrain"}}');var i=t(4848),r=t(8453);const a={id:"quality-gates",title:"The gate: invariants, provenance and mutation",sidebar_label:"The gate (quality gates)",sidebar_position:4,slug:"/quality-gates",description:"What an invariant is in this repo, how to write one, the two house laws with the real case behind each, and the mutation test of the ruler itself."},o="The gate: invariants, provenance and mutation",h={},d=[{value:"Why it exists",id:"why-it-exists",level:2},{value:"What an invariant is here",id:"what-an-invariant-is-here",level:2},{value:"Severity",id:"severity",level:3},{value:"The two house laws",id:"the-two-house-laws",level:2},{value:"Law 1 \u2014 Intention that doesn't become an invariant is optimized away",id:"law-1",level:3},{value:"Law 2 \u2014 A ceiling without provenance is an opinion",id:"law-2",level:3},{value:"Mutation test of the ruler itself",id:"mutation-test",level:2},{value:"The case: 20/22 green with the fix removed",id:"the-case-20-22-green",level:3},{value:"It was not an isolated case \u2014 there were three",id:"not-an-isolated-case",level:3},{value:"Mutation as a first-class thing: ui-check.mjs",id:"mutation-first-class",level:3},{value:"How to write an invariant",id:"how-to-write-an-invariant",level:2},{value:"Anti-patterns that have already cost dearly here",id:"anti-patterns",level:3},{value:"This page is the doctrine. The step-by-step is a skill",id:"doctrine-vs-skill",level:2},{value:"Running the gate",id:"running-the-gate",level:2}];function c(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:["\n",(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"the-gate-invariants-provenance-and-mutation",children:"The gate: invariants, provenance and mutation"})}),"\n",(0,i.jsxs)(n.p,{children:["The gate of this repository is a file: ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs"}),". It runs in pure node\nand exits with code 1 if any ",(0,i.jsx)(n.strong,{children:"critical"})," invariant fails. It is what CI executes on every\nPR (",(0,i.jsx)(n.code,{children:".github/workflows/ci.yml"}),")."]}),"\n","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs"}),": ",(0,i.jsx)(n.strong,{children:"2,275 lines"}),", ",(0,i.jsx)(n.strong,{children:"65 declared invariant identifiers"}),", with ",(0,i.jsx)(n.strong,{children:"28"})," declared ",(0,i.jsx)(n.code,{children:"skip()"})," paths."]}),"\n",(0,i.jsxs)(n.li,{children:["The harness contains ",(0,i.jsx)(n.strong,{children:"200 scripts"})," in ",(0,i.jsx)(n.code,{children:"tools/eval/"}),", plus ",(0,i.jsx)(n.strong,{children:"54 pipeline scripts"})," in ",(0,i.jsx)(n.code,{children:"tools/"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["The number of critical checks in one run depends on the inputs present on that machine; dated results belong in ",(0,i.jsx)(n.code,{children:"KNOWN-BUGS.md"}),"."]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"grep -o \"put('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l\ngrep -o \"skip('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l\n"})}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Block generated by ",(0,i.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,i.jsx)(n.code,{children:"grep -o \"put('[A-Z0-9_]*'\" tools/eval/invariants.mjs | sort -u | wc -l"})]}),"\n"]}),"\n","\n",(0,i.jsxs)(n.p,{children:["The third item above is the distinction that most confuses newcomers: ",(0,i.jsx)(n.strong,{children:"declared\nidentifier \u2260 evaluated invariant."})," Several become ",(0,i.jsx)(n.code,{children:"skip"})," instead of ",(0,i.jsx)(n.code,{children:"put"})," when their\ninput is missing (the viewmodel auditor's JSON, a GLB, a folder of anims). ",(0,i.jsx)(n.code,{children:"skip"})," is a\n",(0,i.jsx)(n.strong,{children:"green gate by absence of data"}),', and that is why it always carries the reason. See\n"Severity", below.']}),"\n",(0,i.jsx)(n.p,{children:"This page is the most useful one on the site. If you only read one, read this one."}),"\n",(0,i.jsx)(n.h2,{id:"why-it-exists",children:"Why it exists"}),"\n",(0,i.jsxs)(n.p,{children:["From the header of the file itself, ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:5-19"}),":"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["The owner spent 3 days in a cycle where every round fixed one thing and broke\nanother, and we only found out one round later. The cause wasn't lack of care:\nit was the lack of a RULER (quality gate). A critic (human or agent) judges screenshots;\nconsistency and flow are properties of the game ",(0,i.jsx)(n.strong,{children:"IN MOTION"}),", and almost every defect he\nreported is not taste \u2014 it's a violated invariant."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"And the translation, which is the most important thing in this entire repository:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"What the owner said"}),(0,i.jsx)(n.th,{children:"Which invariant it became"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"the hands are floating in the air"'}),(0,i.jsx)(n.td,{children:"hand\u2194grip distance has a ceiling"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"the weapon points downward"'}),(0,i.jsx)(n.td,{children:"the barrel has a maximum angle"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"in ADS I can\'t see the weapon or the crosshair"'}),(0,i.jsx)(n.td,{children:"the weapon has a minimum and maximum area"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"sniper with no zoom"'}),(0,i.jsx)(n.td,{children:"aiming FOV < hip FOV"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"several weapons look the same"'}),(0,i.jsx)(n.td,{children:"silhouettes must differ"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"the bot shoots out of nowhere"'}),(0,i.jsx)(n.td,{children:"damage requires prior LOS"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"there are 2 of them eliminating me"'}),(0,i.jsx)(n.td,{children:"1 killfeed per death"})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:20-21"}),":"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"GOLDEN RULE: nothing gets committed with a RED invariant. And every new bug the owner\nreports becomes an invariant here \u2014 that's how it never comes back."})}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"what-an-invariant-is-here",children:"What an invariant is here"}),"\n",(0,i.jsxs)(n.p,{children:["An invariant is a ",(0,i.jsx)(n.strong,{children:"property of the game that can be measured without a human watching"}),",\nwith a ceiling or a range that has provenance. It is not a unit test: almost no invariant\ntests a function. They measure the ",(0,i.jsx)(n.strong,{children:"state of the game actually running"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Three forms, all present in the file:"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"1. Read from the source code."})," Cheap, runs in milliseconds, catches whole classes of bug.\nReal example, quoted verbatim from ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:1439-1446"})," \u2014 the file's\ncomments are written in Portuguese:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:"// ARM1 \u2014 toda arma com luneta precisa de zoom de verdade. \"Snipers sem zoom\"\n// \xe9 reclama\xe7\xe3o literal; a solu\xe7\xe3o N\xc3O \xe9 tirar a luneta, \xe9 fazer a certa.\nconst bloco = gsrc.slice(0, gsrc.indexOf('};', gsrc.indexOf('const WEAPONS')) + 2);\nconst linhas = bloco.split('\\n').filter((l) => /^\\s*\\w+:\\s*\\{/.test(l));\nconst semZoom = linhas.filter((l) => /scope:\\s*true/.test(l) && !/spreadScope/.test(l))\n .map((l) => l.trim().split(':')[0]);\nput('ARM1', 'toda arma com scope:true declara spreadScope', semZoom.length === 0,\n semZoom.length ? semZoom.join(', ') : `${linhas.length} armas conferidas`);\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"2. Measured in the real game running in node."})," ",(0,i.jsx)(n.code,{children:"tools/eval/harness.mjs"})," boots the real\n",(0,i.jsx)(n.code,{children:"Game"})," class, with the real maps, with DOM/canvas stubbed. It is the ",(0,i.jsx)(n.strong,{children:"production code"}),"\nthat gets measured, not a reimplementation \u2014 ",(0,i.jsx)(n.code,{children:"tools/eval/botsim.mjs:8-9"}),": ",(0,i.jsx)(n.em,{children:'"if the number\nimproves here, it improved in the game"'}),". This is where BOT1\u2013BOT8, MAP1\u2013MAP3, CTF1, MAT1/MAT2,\nFOG1, TEX1, VM14, MOD1/MOD2 come from."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"3. Measured on the geometry of the assets."})," ",(0,i.jsx)(n.code,{children:"vm-mint-audit.mjs"})," opens every weapon GLB with\nits own GLB parser and projects the viewmodel onto the screen. This is where VM1\u2013VM19 come from."]}),"\n",(0,i.jsxs)(n.p,{children:["What does ",(0,i.jsx)(n.strong,{children:"not"})," belong here: an invariant that requires browser pixels. Those are marked\n",(0,i.jsx)(n.code,{children:"browser"})," and are skipped, with the reason stated \u2014 SwiftShader costs ~4 min per map load\non this machine (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:99"}),")."]}),"\n",(0,i.jsx)(n.h3,{id:"severity",children:"Severity"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"put(id, desc, ok, evid, sev)"})," accepts ",(0,i.jsx)(n.code,{children:"'crit'"})," (the default) or ",(0,i.jsx)(n.code,{children:"'warn'"}),"\n(",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:81-82"}),"). A red critical fails the PR. Warn is measured noise that\nsomeone needs to look at but doesn't block \u2014 it's where BOT1/BOT2/BOT3/BOT6/BOT7, ARM4 and\nARM5 live. ",(0,i.jsx)(n.code,{children:"skip()"})," is the third state, and it is ",(0,i.jsx)(n.strong,{children:"dangerous"}),": a green gate by absence of\ndata. That is why every ",(0,i.jsx)(n.code,{children:"skip"})," carries the reason."]}),"\n",(0,i.jsx)(n.h2,{id:"the-two-house-laws",children:"The two house laws"}),"\n",(0,i.jsx)(n.h3,{id:"law-1",children:"Law 1 \u2014 Intention that doesn't become an invariant is optimized away"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsxs)(n.strong,{children:["Source: ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:452-461"}),"."]})," The case, verbatim:"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["the previous round took the gate from ",(0,i.jsx)(n.strong,{children:"16/21 to 19/21 without loosening a single ceiling"})," and\nwas still ",(0,i.jsx)(n.strong,{children:"REJECTED"})," by the owner, because to close VM5/VM10 it ",(0,i.jsxs)(n.strong,{children:["ZEROED the\n",(0,i.jsx)(n.code,{children:"VM_OFF"})," y"]}),' and silently changed the look. No invariant encoded "where the\nmuzzle sits", so the metric was optimized and the INTENT was destroyed. Goodhart\'s\nlaw, in full. ',(0,i.jsx)(n.strong,{children:"INTENTION THAT DOESN'T BECOME AN INVARIANT IS OPTIMIZED AWAY."})]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Read again what happened, because it is counterintuitive: the agent ",(0,i.jsx)(n.strong,{children:"did not cheat"}),". It\ndidn't loosen any ceiling. It genuinely raised the score. And the result was worse, because\n",(0,i.jsx)(n.code,{children:"VM_OFF[1]"})," is the term that ",(0,i.jsx)(n.strong,{children:"dominates the weapon's position on screen"})," \u2014 ",(0,i.jsx)(n.code,{children:"public/js/game.js:555"}),"\ndeclares ",(0,i.jsx)(n.code,{children:"VM_OFF = [0.03, -0.1000, 0]"}),", and ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:1163"})," measures the\nsensitivity: ",(0,i.jsx)(n.em,{children:'"removing recuoZ moves the grip 3,5 cm; removing VM_OFF moves it 23 cm"'}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Zeroing that term closed two invariants and erased the aesthetic decision the owner had\nmade \u2014 which wasn't written anywhere the ruler could read."}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"The fix was not punishing the agent. It was writing the intention as an invariant."})," Today\nthere is VM12 (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:497"}),"): ",(0,i.jsx)(n.em,{children:'"CS 1.6 look: muzzle RIGHT below\nthe crosshair (y between 0,50 and 0,62) in the 2 aspects"'}),". With it in place, the same\noptimization goes ",(0,i.jsx)(n.strong,{children:"red"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"And the operational consequence, from the same comment:"}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Whoever wants to change the look has to change ",(0,i.jsx)(n.strong,{children:"THIS ceiling"})," explicitly, in a diff the\nowner sees, instead of touching ",(0,i.jsx)(n.code,{children:"VM_OFF"}),' and reporting "+3 invariants".']}),"\n"]}),"\n",(0,i.jsx)(n.admonition,{title:"What this means for your PR",type:"tip",children:(0,i.jsxs)(n.p,{children:["If your change improves the gate's score, the first question is: ",(0,i.jsx)(n.strong,{children:"what did I change that\nthe gate doesn't look at?"}),' If the answer is "the look", "the feel" or "the vibe", write the\ninvariant before sending the PR \u2014 or explain in the PR why it doesn\'t fit.']})}),"\n",(0,i.jsx)(n.h3,{id:"law-2",children:"Law 2 \u2014 A ceiling without provenance is an opinion"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsxs)(n.strong,{children:["Source: ",(0,i.jsx)(n.code,{children:"tools/eval/ref-measure.py:1-40"}),"."]})," That docstring is the house doctrine. The case:"]}),"\n",(0,i.jsxs)(n.p,{children:["For ",(0,i.jsx)(n.strong,{children:"three days"})," the weapons gate was solved against ",(0,i.jsx)(n.strong,{children:"asserted"})," numbers:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["VM12 required ",(0,i.jsx)(n.em,{children:'"muzzle at y \u2265 0,66"'}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"vmattach.js"})," doc said ",(0,i.jsx)(n.em,{children:'"ENTIRE stock in the corner"'}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Neither of the two was measured on any image. According to ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:461-463"}),",\nthe 0,66 floor came from a comment in ",(0,i.jsx)(n.code,{children:"public/js/vmattach.js"})," \u2014 ",(0,i.jsx)(n.em,{children:'"the muzzle sits at ~0,66H"'})," \u2014\nwhich in turn came from a video someone watched. (The gate's comment points to\n",(0,i.jsx)(n.code,{children:"vmattach.js:387-392"}),"; today the text is at ",(0,i.jsx)(n.code,{children:"vmattach.js:395"}),", because the file moved. That is\nexactly why ",(0,i.jsx)(n.code,{children:"ARCH.md"})," is generated \u2014 see ",(0,i.jsx)(n.a,{href:"/docs/en/architecture",children:"Architecture"}),".)"]}),"\n",(0,i.jsxs)(n.p,{children:["The owner looked at the result and said, verbatim (",(0,i.jsx)(n.code,{children:"ref-measure.py:14-17"}),"):"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:'"it looks different from CS 1.6 and Quake and UT; in those 3 the weapon is always in the\nbottom-right corner and the stock is always OUTSIDE; after 3 days and an entire folder of\nreference neither you nor Kimi understood that."'}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Then the frames were ",(0,i.jsx)(n.strong,{children:"measured"}),". ",(0,i.jsx)(n.code,{children:"tools/eval/ref-measure.py"})," does color segmentation on the\nbottom-right quadrant, takes the largest connected component, and writes\n",(0,i.jsx)(n.code,{children:"tools/eval/ref_viewmodel.json"}),". Result:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Frame"}),(0,i.jsx)(n.th,{children:"Muzzle (x, y)"}),(0,i.jsx)(n.th,{style:{textAlign:"right"},children:"Screen area"}),(0,i.jsx)(n.th,{style:{textAlign:"right"},children:"Axis angle"}),(0,i.jsx)(n.th,{children:"Crosses the right edge?"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"cs16_ak_dust.jpg"})}),(0,i.jsxs)(n.td,{children:["0,564 ; ",(0,i.jsx)(n.strong,{children:"0,513"})]}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"9,76%"}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"28,0\xb0"}),(0,i.jsx)(n.td,{children:"yes"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"cs16_m4_dust.jpg"})}),(0,i.jsxs)(n.td,{children:["0,569 ; ",(0,i.jsx)(n.strong,{children:"0,598"})]}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"9,78%"}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"34,8\xb0"}),(0,i.jsx)(n.td,{children:"yes"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"valorant_vandal.jpg"})}),(0,i.jsxs)(n.td,{children:["0,648 ; ",(0,i.jsx)(n.strong,{children:"0,587"})]}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"13,09%"}),(0,i.jsx)(n.td,{style:{textAlign:"right"},children:"4,6\xb0"}),(0,i.jsx)(n.td,{children:"yes"})]})]})]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Both asserted numbers were wrong:"})}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["The CS 1.6 muzzle sits at ",(0,i.jsx)(n.strong,{children:"0,513\u20130,598"})," \u2014 right below the crosshair (0,5), 1 to 10\npercentage points below center. Not at 0,66\u20130,93. The wrong floor was keeping our\nweapon ",(0,i.jsx)(n.strong,{children:"sunk"})," at 0,667\u20130,816 (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:472-475"}),")."]}),"\n",(0,i.jsxs)(n.li,{children:["The stock ",(0,i.jsx)(n.strong,{children:"EXITS through the corner"})," in all 3 frames. Exiting is the standard, not the defect\n(",(0,i.jsx)(n.code,{children:"ref_viewmodel.json"})," \u2192 ",(0,i.jsx)(n.code,{children:"faixas.cruzaBordaDireita: true"})," in all 3)."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["And the collateral damage: with the false ceiling, the previous round's solver ",(0,i.jsx)(n.em,{children:'"proved"'})," that 3%\nof area was infeasible. The proof was correct ",(0,i.jsx)(n.strong,{children:"against that ceiling"})," \u2014 and it was the ceiling that\nwas false (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:476-478"}),")."]}),"\n",(0,i.jsxs)(n.p,{children:["The rule that remained, ",(0,i.jsx)(n.code,{children:"tools/eval/ref-measure.py:21-22"}),":"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"AN INVARIANT CEILING ONLY GETS IN WITH PROVENANCE \u2014 reference file, measured pixel, and\nthis script reproducing the number. A number without an image is an opinion."})}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Today the framing invariants carry the provenance in their own text: VM1 (range\n0,50\u20130,60, ref 0,520\u20130,565), VM3 (22\u201342\xb0, ref 28,0\xb0 and 34,8\xb0), VM5 (6\u201316%, ref\n9,76\u201313,09%), VM12 (0,50\u20130,62, ref 0,513\u20130,598), VM16 (slice at the right edge 0,02\u20130,20,\nref 0,053\u20130,095)."}),"\n",(0,i.jsx)(n.admonition,{title:"Provenance includes admitting what the image does NOT measure",type:"note",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:599-602"}),' refuses to create a ceiling for "how much of the weapon\nsits outside the frame", because what is outside is invisible in the photo \u2014 there is no way to\nknow whether the AK\'s stock ends 5 cm or 50 cm past the edge. The numbers remain in the JSON as\n',(0,i.jsx)(n.strong,{children:"evidence, without a gate"}),". That is provenance taken seriously: the ruler says where it stops\nknowing."]})}),"\n",(0,i.jsxs)(n.p,{children:["And the same rigor bites whoever wrote the ruler, in the most uncomfortable case possible: ",(0,i.jsx)(n.strong,{children:"the\ncharacter reference photos arrived, were measured, and were REJECTED by the ruler\nitself."})," ",(0,i.jsx)(n.code,{children:"tools/eval/char-probe.mjs:25-45"})," tells the whole episode \u2014 ",(0,i.jsx)(n.code,{children:"references/funkeiros/"}),"\nhas 23 files and ",(0,i.jsx)(n.code,{children:"references/palhacos/"})," has 21, all run through ",(0,i.jsx)(n.code,{children:"ref-body.py"}),", with the\nmasks ",(0,i.jsx)(n.strong,{children:"looked at"})," (",(0,i.jsx)(n.code,{children:"--masks"}),"). The verdict, said to our face by the comment itself: they are\nselfies and close-ups; the heuristic segmentation returns the hand, a piece of jacket or the hair\nof someone else in the background, and the shoulder/height ratio comes out between ",(0,i.jsx)(n.strong,{children:"0,42 and 3,78"})," when a human\nmeasures 0,259. About ~1 usable full-body photo remains \u2014 that is not a sample."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"ref-body.py"})," requires ",(0,i.jsx)(n.strong,{children:"6 accepted photos"})," for a ceiling to become measured, and it ",(0,i.jsx)(n.strong,{children:"says why\nit didn't"}),". So the absolute ceiling of CHR1 remains a ",(0,i.jsx)(n.strong,{children:"published fallback"})," (Drillis &\nContini 1966, via Winter), declared as such in the ",(0,i.jsx)(n.code,{children:"procedencia"})," field of the JSON and in the\nreport column."]}),"\n",(0,i.jsxs)(n.p,{children:["Notice what that means: having the photo is ",(0,i.jsx)(n.strong,{children:"not"})," having the measurement. It was easier to accept\nthat the data was bad than to promote a fragile measurement to a ceiling \u2014 and that is Law 2\napplied against the interest of whoever wrote the ruler."]}),"\n",(0,i.jsxs)(n.admonition,{type:"warning",children:[(0,i.jsxs)(n.mdxAdmonitionTitle,{children:[(0,i.jsx)(n.code,{children:"references/"})," does NOT come with the clone \u2014 and that is a decision, not carelessness"]}),(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"git ls-files references"})," returns ",(0,i.jsx)(n.strong,{children:"zero"}),". On 04/08/2026 the entire folder was\nuntracked by the owner's decision (\"references can stay local because we're going to build\nlocally\"): they are the UI target screens and the viewmodel reference frames, and they live only on\nhis machine."]}),(0,i.jsxs)(n.p,{children:["What ",(0,i.jsx)(n.strong,{children:"survives the clone are the NUMBERS measured from them"}),": ",(0,i.jsx)(n.code,{children:"tools/eval/ref_ui.json"})," and\n",(0,i.jsx)(n.code,{children:"tools/eval/ref_viewmodel.json"})," are versioned. That is the contract \u2014 if a ruler of yours\nneeds to run in CI, it reads the JSON, never the PNG. A ruler that opens an image from\n",(0,i.jsx)(n.code,{children:"references/"})," goes red on every machine that isn't the owner's, and red-by-environment is\nthe worst kind: it teaches whoever works here to ignore red."]})]}),"\n",(0,i.jsx)(n.h2,{id:"mutation-test",children:"Mutation test of the ruler itself"}),"\n",(0,i.jsx)(n.p,{children:"This is the part almost no project has, and it is where this repository is genuinely\ndifferent."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"A gate that doesn't move when you break the code on purpose is blind."})}),"\n",(0,i.jsxs)(n.p,{children:["The way to find that out is to mutate: take the fixed code, ",(0,i.jsx)(n.strong,{children:"undo the fix on\npurpose"}),", run the gate, and see whether it goes red. If it stays green, the gate is not\nmeasuring what you think it measures."]}),"\n",(0,i.jsx)(n.h3,{id:"the-case-20-22-green",children:"The case: 20/22 green with the fix removed"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsxs)(n.strong,{children:["Source: ",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:910-920"}),"."]})}),"\n",(0,i.jsxs)(n.p,{children:["The context: ",(0,i.jsx)(n.code,{children:"public/js/game.js:577"})," declares"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:"const vmOffY = (aspect) => VM_OFF[1] * ((16 / 9) / (aspect || 16 / 9));\n"})}),"\n",(0,i.jsxs)(n.p,{children:["It is the per-aspect vertical framing fix \u2014 the reason the weapon sits in the same\nplace in 16:9 and in 3:2 (the owner plays in 3:2). It is ",(0,i.jsx)(n.strong,{children:"called"})," in the Y argument of\n",(0,i.jsx)(n.code,{children:"this.vm.root.position.set(...)"}),", at ",(0,i.jsx)(n.code,{children:"public/js/game.js:4873"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"The hole, measured in 08/2026:"}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["the ",(0,i.jsx)(n.code,{children:"vmOff"})," step only checked ",(0,i.jsx)(n.code,{children:"/this\\.vm\\.root\\.position\\.set\\(\\s*VM_OFF\\[0\\]/"})," \u2014 the X\nterm. The Y term was checked by no one, and the auditor (",(0,i.jsx)(n.code,{children:"vm-mint-audit.mjs:196"}),",\n",(0,i.jsx)(n.code,{children:"loadOffYFn"}),") reads the ",(0,i.jsx)(n.strong,{children:"DECLARATION"})," ",(0,i.jsx)(n.code,{children:"const vmOffY = (aspect) => ..."})," by regex ",(0,i.jsx)(n.strong,{children:"without ever\nasking whether anyone CALLS it"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Result: swapping in ",(0,i.jsx)(n.code,{children:"game.js"})," the call ",(0,i.jsx)(n.code,{children:"vmOffY(...)"})," for ",(0,i.jsx)(n.code,{children:"VM_OFF[1]"})," in the Y argument\n\u2014 that is, ",(0,i.jsx)(n.strong,{children:"removing the per-aspect vertical framing fix entirely"})," \u2014 the\nwhole gate stayed ",(0,i.jsx)(n.strong,{children:"GREEN (20/22, with VM9, VM10, VM12 and VM15 all green)"}),". A\ngate that cannot tell the fixed build from the build without the fix is measuring\nnothing."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Notice the mechanism of the error, because it repeats in any language: ",(0,i.jsxs)(n.strong,{children:["the invariant\nwas reading the ",(0,i.jsx)(n.em,{children:"declaration"})," of a constant, and not the ",(0,i.jsx)(n.em,{children:"use"}),"."]})," Declaring and not calling is the\ncheapest way for a fix to vanish with the gate green."]}),"\n",(0,i.jsxs)(n.p,{children:["The repair was surgical and worth copying. AUD1 today separates the three arguments of\n",(0,i.jsx)(n.code,{children:"position.set(...)"})," with a ",(0,i.jsx)(n.strong,{children:"parenthesis scanner"})," \u2014 not ",(0,i.jsx)(n.code,{children:"split(',')"}),", which would cut\ninside the function call \u2014 and requires ",(0,i.jsx)(n.strong,{children:"by name"})," that the Y argument calls ",(0,i.jsx)(n.code,{children:"vmOffY("}),".\nAnd it closes the other path along with it (",(0,i.jsx)(n.code,{children:"tools/eval/invariants.mjs:1148-1151"}),"): the formula of\n",(0,i.jsx)(n.code,{children:"vmOffY"})," is ",(0,i.jsxs)(n.strong,{children:["read from ",(0,i.jsx)(n.code,{children:"game.js"})," and evaluated"]})," at 16/9, and it has to yield exactly ",(0,i.jsx)(n.code,{children:"VM_OFF[1]"}),"."]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["The two checks together cover the two ways for the fix to vanish: ",(0,i.jsx)(n.strong,{children:"deleting the CALL"}),"\n(mutation measured in 08/2026) or ",(0,i.jsx)(n.strong,{children:"tampering with the FORMULA"}),"."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"not-an-isolated-case",children:"It was not an isolated case \u2014 there were three"}),"\n",(0,i.jsx)(n.p,{children:"The same hole showed up in two other places, and each one became a new AUD1 step:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Mutation"}),(0,i.jsx)(n.th,{children:"Score with the fix undone"}),(0,i.jsx)(n.th,{children:"Cause of the false green"}),(0,i.jsx)(n.th,{children:"Where"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["Swap ",(0,i.jsx)(n.code,{children:"vmOffY(...)"})," for ",(0,i.jsx)(n.code,{children:"VM_OFF[1]"})," in the Y argument"]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"20/22 green"})}),(0,i.jsx)(n.td,{children:"the invariant read the declaration, not the use"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"invariants.mjs:910-920"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["Swap ",(0,i.jsx)(n.code,{children:"g.rotation.set(pit, yaw, t.roll)"})," for ",(0,i.jsx)(n.code,{children:"g.rotation.set(0, 0, t.roll)"})]}),(0,i.jsx)(n.td,{children:"green"}),(0,i.jsxs)(n.td,{children:["the ",(0,i.jsx)(n.code,{children:"VM_FRAME.cls"})," table still holds the angles, and the three mirrors still match ",(0,i.jsx)(n.strong,{children:"each other"})]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"invariants.mjs:932-944"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["Delete ",(0,i.jsx)(n.code,{children:"* (weaponCFG(id).vm ?? 1)"})," from the mesh scale"]}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.strong,{children:"28/37 green, AUD1 included"}),' ("worst \u0394scale 0.0004")']}),(0,i.jsxs)(n.td,{children:["both ends read ",(0,i.jsx)(n.code,{children:"vm"})," from ",(0,i.jsx)(n.code,{children:"weapons.js"}),"; ",(0,i.jsxs)(n.strong,{children:[(0,i.jsx)(n.code,{children:"game.js"})," is never asked"]})," \u2014 it was the auditor checking itself"]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"invariants.mjs:971-975"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:["Mutate ",(0,i.jsx)(n.code,{children:"this._adsPose['pistol']"})]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.strong,{children:"20/22 green"})}),(0,i.jsx)(n.td,{children:"ADS had no invariant at all"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"invariants.mjs:1185"})})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"The common pattern of the four is the same, and it is what you should look for in your invariant:"}),"\n",(0,i.jsx)(n.admonition,{title:"The false-green pattern",type:"danger",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"The ruler is checking a copy of the rule instead of the game."})," Whether because it reads the declaration\nand not the use, or because it compares two mirrors that read the same source, or because the\nparameter table stays correct while no one applies it. If the two ends of your\ncomparison can stay consistent ",(0,i.jsx)(n.strong,{children:"without going through the production code"}),", your\ninvariant is blind."]})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"tools/eval/mat-check.mjs:18-27"})," solves this in the most direct way possible: the body of\n",(0,i.jsx)(n.code,{children:"fixVmMaterials"})," is ",(0,i.jsxs)(n.strong,{children:["cut out of ",(0,i.jsx)(n.code,{children:"game.js"})," and executed"]})," against a probe material. If the\ncode changes, the ruler changes with it. ",(0,i.jsx)(n.em,{children:'"a ruler that carries a COPY of the rule lies on the day\nthe rule changes."'})]}),"\n",(0,i.jsxs)(n.h3,{id:"mutation-first-class",children:["Mutation as a first-class thing: ",(0,i.jsx)(n.code,{children:"ui-check.mjs"})]}),"\n",(0,i.jsxs)(n.p,{children:["The UI harness has a ",(0,i.jsx)(n.strong,{children:"versioned mutation table"}),", and each one declares which gate\nhas to go red. ",(0,i.jsx)(n.code,{children:"tools/eval/ui-check.mjs:1046-1050"}),":"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["Each mutation UNDOES one of this round's fixes (or punches through a gate on purpose) and says\nwhich gate MUST go red. ",(0,i.jsx)(n.strong,{children:"A ruler that does not fail the previous version of\nits own file is not a ruler, it is decoration."})]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Running one:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"MUT=ui1_ctf_scrim_fraco node tools/eval/ui-check.mjs # expects UI1 to go RED\nMUT=ui3_prompt_na_mira node tools/eval/ui-check.mjs # expects UI3 to go RED\nMUT=ui2_prompt_eterno node tools/eval/ui-check.mjs # expects UI2 to go RED\nMUT=ui4_ctf_sem_relogio node tools/eval/ui-check.mjs # expects UI4 to go RED\n"})}),"\n",(0,i.jsxs)(n.p,{children:["The 7 mutations are in ",(0,i.jsx)(n.code,{children:"tools/eval/ui-check.mjs:1051-1134"}),". Two mechanics: ",(0,i.jsx)(n.code,{children:"css"})," rewrites\n",(0,i.jsx)(n.code,{children:"public/style.css"})," ",(0,i.jsx)(n.strong,{children:"read in memory"})," (never on disk \u2014 other agents are editing the\nfile right now), and ",(0,i.jsx)(n.code,{children:"sim"})," monkey-patches the already-booted ",(0,i.jsx)(n.code,{children:"Game"})," object. If the ",(0,i.jsx)(n.code,{children:"css"})," mutation\nmatches nothing, the script exits with code 2 saying ",(0,i.jsx)(n.em,{children:'"the CSS changed shape"'})," \u2014 because a\nmutation that doesn't apply is also a false green (",(0,i.jsx)(n.code,{children:"ui-check.mjs:1164"}),")."]}),"\n",(0,i.jsx)(n.h2,{id:"how-to-write-an-invariant",children:"How to write an invariant"}),"\n",(0,i.jsx)(n.p,{children:"Checklist, in order:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Start from the defect's sentence."})," Verbatim, with the words of whoever complained. Every\nharness in this codebase starts that way, and it is not style: it is what keeps the invariant from\nmeasuring something else. See the header of ",(0,i.jsx)(n.code,{children:"tools/eval/map-check.mjs:5-12"})," \u2014 five sentences from the owner,\nfive invariants."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Translate it into a measurable quantity."}),' "the players are SUBMERGED UNDER THE\nSTATUE" \u2192 ',(0,i.jsx)(n.em,{children:"there is visible map geometry whose top rises more than 0,30 m above the floor\nlocal to that point"})," (MAP1). Note that the operational definition includes ",(0,i.jsx)(n.strong,{children:"why 0,30 m"}),':\nit is the step the body climbs; above that it is not "stepping over", it is "being inside".']}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Find the ceiling's provenance."})," Reference file + measured pixel + script that\nreproduces it. If it doesn't exist, ",(0,i.jsx)(n.strong,{children:"say it is a fallback"})," and cite the published source, as C1\ndoes. Never invent the number."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Measure the production code, not a copy of it."})," Import the real module, cut the\nfunction out of the file and execute it, or require the call by name in the source text."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Mutate and confirm it goes red."})," Undo the fix you just made and\nrun the gate. If it stays green, your invariant is blind \u2014 go back to step 4. If it can\nbe automated, register the mutation in a table, as ",(0,i.jsx)(n.code,{children:"ui-check.mjs"})," does."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Write the evidence, not just the boolean."})," The fourth argument of ",(0,i.jsx)(n.code,{children:"put()"})," is what\nsomeone will read three months from now: ",(0,i.jsx)(n.code,{children:'"0,504 a 0,619 da altura em 52 medidas | 0 fora da faixa"'})," is useful; ",(0,i.jsx)(n.code,{children:'"ok"'})," is not."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Write the provenance comment above it."})," In Portuguese, saying what\nhappened when the number was wrong. That comment is what keeps the next round\nfrom redoing the mistake."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"anti-patterns",children:"Anti-patterns that have already cost dearly here"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Anti-pattern"}),(0,i.jsx)(n.th,{children:"What it produced"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Reading a constant's declaration instead of its use"}),(0,i.jsx)(n.td,{children:"20/22 green with the fix removed"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Two mirrors that read the same source"}),(0,i.jsx)(n.td,{children:'28/37 green, "worst \u0394scale 0.0004", with the knob turned off'})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Format adapter silently broken"}),(0,i.jsxs)(n.td,{children:["VM1\u2013VM6 have been ",(0,i.jsx)(n.strong,{children:"SKIPPED since the auditor exists"})," \u2014 6 viewmodel invariants that never ran once (",(0,i.jsx)(n.code,{children:"invariants.mjs:121-127"}),")"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Measuring the gap against the wrong local floor"}),(0,i.jsxs)(n.td,{children:["a pickup inside the pool reported gap ",(0,i.jsx)(n.strong,{children:"0,0000 \u2014 GREEN"})," (",(0,i.jsx)(n.code,{children:"pickup-check.mjs:20-23"}),")"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:'"waypoint \u2264 3 m" as a reachability proxy'}),(0,i.jsxs)(n.td,{children:["74 false positives and green in a closed pocket (",(0,i.jsx)(n.code,{children:"pickup-check.mjs:34-42"}),")"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Floor without ceiling"}),(0,i.jsxs)(n.td,{children:['"muzzle \u2265 0,66" accepts the muzzle at 0,95 (weapon in the basement) \u2014 that is how we got to 0,816 (',(0,i.jsx)(n.code,{children:"invariants.mjs:432-434"}),")"]})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"doctrine-vs-skill",children:"This page is the doctrine. The step-by-step is a skill"}),"\n",(0,i.jsxs)(n.p,{children:["What to do, in order, when someone reports a defect \u2014 reproduce, measure before\nfixing, refute the obvious guess, mutate the ruler, run the gate in the right order and report\nwhat was ",(0,i.jsx)(n.strong,{children:"not"})," verified \u2014 is in ",(0,i.jsx)(n.code,{children:".claude/skills/bug-hunt/SKILL.md"}),", with the real case\nthat paid for each rule. It is written for agents ",(0,i.jsx)(n.strong,{children:"and"})," for people, and it points back to\nthis page instead of repeating it."]}),"\n",(0,i.jsx)(n.h2,{id:"running-the-gate",children:"Running the gate"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"node tools/eval/invariants.mjs # everything that runs without a browser\nnode tools/eval/invariants.mjs --json # machine-readable output\nnpm run check # syntax + gate + vm + recoil + bots\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Current production, data, and debt sources: ",(0,i.jsx)(n.a,{href:"./status",children:"Current state"}),"."]})]})}function l(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},8453(e,n,t){t.d(n,{R:()=>a,x:()=>o});var s=t(6540);const i={},r=s.createContext(i);function a(e){const n=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/en/assets/js/ed1a65a0.28d4867f.js b/public/docs/en/assets/js/ed1a65a0.813e3890.js similarity index 99% rename from public/docs/en/assets/js/ed1a65a0.28d4867f.js rename to public/docs/en/assets/js/ed1a65a0.813e3890.js index a490a03b0..67fcd46f8 100644 --- a/public/docs/en/assets/js/ed1a65a0.28d4867f.js +++ b/public/docs/en/assets/js/ed1a65a0.813e3890.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[502],{8909(e,n,s){s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>l,frontMatter:()=>d,metadata:()=>t,toc:()=>h});const t=JSON.parse('{"id":"stack","title":"Stack and tools","description":"Three.js/WebGL with no build, Astro on Vercel, Supabase, the asset generation pipeline (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform and the agent skills \u2014 each with the version read from package.json.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/stack.md","sourceDirName":".","slug":"/stack","permalink":"/docs/en/stack","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/stack.md","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"id":"stack","title":"Stack and tools","sidebar_label":"Stack and tools","sidebar_position":2,"slug":"/stack","description":"Three.js/WebGL with no build, Astro on Vercel, Supabase, the asset generation pipeline (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform and the agent skills \u2014 each with the version read from package.json."},"sidebar":"dev","previous":{"title":"Getting started","permalink":"/docs/en/"},"next":{"title":"AI instrumentation","permalink":"/docs/en/ai-instrumentation"}}');var r=s(4848),i=s(8453);const d={id:"stack",title:"Stack and tools",sidebar_label:"Stack and tools",sidebar_position:2,slug:"/stack",description:"Three.js/WebGL with no build, Astro on Vercel, Supabase, the asset generation pipeline (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform and the agent skills \u2014 each with the version read from package.json."},o="Stack and tools",c={},h=[{value:"The two zones, and why the boundary is hard",id:"the-two-zones",level:2},{value:"public/ \u2014 the GAME: Three.js, WebGL, zero build",id:"public-the-game",level:3},{value:"src/ \u2014 the SITE: Astro with SSR on Vercel",id:"src-the-site",level:3},{value:"Database \u2014 managed Postgres, RLS and telemetry",id:"database",level:3},{value:"Asset generation \u2014 what is AI-generated, and by which service",id:"asset-generation",level:2},{value:"Characters: mint.gg",id:"characters-mintgg",level:3},{value:"3D props: Tripo3D and Meshy",id:"props-3d",level:3},{value:"2D art: OpenRouter",id:"art-2d-openrouter",level:3},{value:"The keys",id:"the-keys",level:3},{value:"GLB optimization: gltf-transform and meshoptimizer",id:"glb-optimization",level:2},{value:"Playwright \u2014 every harness that needs a browser",id:"playwright",level:2},{value:"Agent skills",id:"agent-skills",level:2},{value:"The gauntlet loop",id:"the-gauntlet-loop",level:3},{value:"The documentation",id:"the-documentation",level:2}];function a(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:["\n",(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"stack-and-tools",children:"Stack and tools"})}),"\n",(0,r.jsxs)(n.p,{children:["This page answers the question ",(0,r.jsx)(n.em,{children:'"what is this made with?"'})," \u2014 and it answers with the ",(0,r.jsx)(n.strong,{children:"declared\nversion"}),", not the remembered one. The table below is generated by ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),"\nfrom ",(0,r.jsx)(n.code,{children:"package.json"}),", ",(0,r.jsx)(n.code,{children:"docs/package.json"})," and the vendored Three.js itself."]}),"\n","\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Layer"}),(0,r.jsx)(n.th,{children:"Tool"}),(0,r.jsx)(n.th,{children:"Version"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"3D engine (WebGL)"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"Three.js"}),", vendored"]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"r160"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Game"}),(0,r.jsxs)(n.td,{children:["vanilla ES modules, ",(0,r.jsx)(n.strong,{children:"zero build"})]}),(0,r.jsx)(n.td,{children:"44 files"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Site"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"Astro"})," with SSR"]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^7.1.1"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Hosting"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"Vercel"})," adapter"]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^11.0.3"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Database"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"managed Postgres"})," (RLS; private schema)"]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^2.110.7"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Browser checks"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Playwright"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^1.62.1"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GLB pipeline"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"gltf-transform"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^4.4.1"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Mesh compression"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"meshoptimizer"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^1.2.0"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Images"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"sharp"})," \xb7 ",(0,r.jsx)(n.strong,{children:"resvg"})]}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"^0.35.3"})," \xb7 ",(0,r.jsx)(n.code,{children:"^2.6.2"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"This documentation"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Docusaurus"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"3.6.3"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"CI runtime"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Node"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"22"})})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["Three.js comes from ",(0,r.jsx)(n.code,{children:"public/vendor/three.module.js"}),". Of the scripts in ",(0,r.jsx)(n.code,{children:"tools/"}),", ",(0,r.jsx)(n.strong,{children:"109"})," import Playwright, ",(0,r.jsx)(n.strong,{children:"37"})," import gltf-transform, and ",(0,r.jsx)(n.strong,{children:"4"})," import meshoptimizer."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Block generated by ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(n.code,{children:"dependencies/devDependencies do package.json \xb7 REVISION de public/vendor/three.module.js"})]}),"\n"]}),"\n","\n",(0,r.jsx)(n.h2,{id:"the-two-zones",children:"The two zones, and why the boundary is hard"}),"\n",(0,r.jsxs)(n.p,{children:["The repository has ",(0,r.jsx)(n.strong,{children:"two applications with opposite rules"}),", and almost every misunderstanding from\nnewcomers is born from treating them as one."]}),"\n",(0,r.jsxs)(n.h3,{id:"public-the-game",children:[(0,r.jsx)(n.code,{children:"public/"})," \u2014 the GAME: Three.js, WebGL, zero build"]}),"\n",(0,r.jsxs)(n.p,{children:["The game is ",(0,r.jsx)(n.strong,{children:"vanilla JavaScript with ES modules served raw"}),". There is no bundler, no\ntranspiler, no build step. The browser downloads ",(0,r.jsx)(n.code,{children:"public/js/game.js"})," exactly as it is in the\nrepository."]}),"\n",(0,r.jsxs)(n.p,{children:["This is a ",(0,r.jsx)(n.strong,{children:"design decision, not laziness"}),", and it pays off in three places:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"The game runs by dragging the folder onto any static host."})," It does not depend on Astro,\ndoes not depend on Vercel, does not depend on npm at runtime. It is what makes it viable to ship on a\nportal (CrazyGames, itch) without rewriting anything."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:["The harness can boot the ",(0,r.jsx)(n.code,{children:"Game"})," class in pure node."]})," ",(0,r.jsx)(n.code,{children:"tools/eval/harness.mjs"}),"\nimports the ",(0,r.jsx)(n.strong,{children:"production code"})," with DOM and canvas stubbed, and measures the real game in\nseconds. A bundler in the middle would break that \u2014 and without that there is no gate."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:[(0,r.jsx)(n.code,{children:"node --check"})," on each file is a complete syntax test"]})," (",(0,r.jsx)(n.code,{children:"npm run syntax"}),"),\nbecause the file node parses is byte for byte what the browser executes."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["The price, which is also real: ",(0,r.jsx)(n.strong,{children:"cache"}),". Without a build there is no hash in the filename, so\ninvalidation is manual \u2014 the import map's ",(0,r.jsx)(n.code,{children:"?v="}),". The rule and what it has already cost are in\n",(0,r.jsx)(n.a,{href:"/docs/en/#the-two-zones",children:"Getting started"}),", in one place only."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Three.js is vendored"})," at ",(0,r.jsx)(n.code,{children:"public/vendor/three.module.js"})," (plus ",(0,r.jsx)(n.code,{children:"vendor/addons/"}),").\nNo CDN and no runtime dependency: the import map points to the local file. Do not\nadd a CDN or a runtime package without opening an issue."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"WebGL is the target, and a weak machine is a requirement."})," There is a ",(0,r.jsx)(n.code,{children:"quality: 'low'"})," path with no\npost-processing and a querystring kill-switch for every risky change (",(0,r.jsx)(n.code,{children:"?bloom=0"}),",\n",(0,r.jsx)(n.code,{children:"?ao=0"}),", ",(0,r.jsx)(n.code,{children:"?fxaa=0"}),", ",(0,r.jsx)(n.code,{children:"?water=0"}),"). Every graphics change that requires extra rendering has to\ndeclare its measured cost."]}),"\n",(0,r.jsxs)(n.h3,{id:"src-the-site",children:[(0,r.jsx)(n.code,{children:"src/"})," \u2014 the SITE: Astro with SSR on Vercel"]}),"\n",(0,r.jsxs)(n.p,{children:["The site is ",(0,r.jsx)(n.a,{href:"https://astro.build",children:"Astro"})," with the Vercel adapter. ",(0,r.jsx)(n.code,{children:"astro.config.mjs"})," is set to\n",(0,r.jsx)(n.code,{children:"output: 'static'"})," ",(0,r.jsx)(n.strong,{children:"with adapter"}),", and the routes that need a server opt in with\n",(0,r.jsx)(n.code,{children:"export const prerender = false"})," one by one \u2014 the case of ",(0,r.jsx)(n.code,{children:"/ranking"}),", ",(0,r.jsx)(n.code,{children:"/u/*"}),",\n",(0,r.jsx)(n.code,{children:"/sitemap.xml"})," and all the ",(0,r.jsx)(n.code,{children:"/api/*"})," routes."]}),"\n",(0,r.jsx)(n.p,{children:"Here a framework is welcome. The rules that apply:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["the Supabase ",(0,r.jsx)(n.code,{children:"service_role"})," lives ",(0,r.jsx)(n.strong,{children:"server-side only"})," and never reaches the browser;"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"site"})," in ",(0,r.jsx)(n.code,{children:"astro.config.mjs"})," is ",(0,r.jsxs)(n.strong,{children:["with ",(0,r.jsx)(n.code,{children:"www"})]}),", and every canonical comes from there;"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"vercel.json"})," carries the security headers (CSP, HSTS, nosniff, Referrer-Policy,\nPermissions-Policy) and the CDN cache."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["And the gotcha that costs everyone their first hour: ",(0,r.jsxs)(n.strong,{children:[(0,r.jsx)(n.code,{children:"src/pages/index.astro"})," IS the\ngame"]}),", served at the ",(0,r.jsx)(n.code,{children:"/"})," route. There is no ",(0,r.jsx)(n.code,{children:"public/index.html"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"database",children:"Database \u2014 managed Postgres, RLS and telemetry"}),"\n",(0,r.jsxs)(n.p,{children:["The ranking and the telemetry live in a managed Postgres. Schema and migrations are\nprivate (outside the repo \u2014 a security decision); the runtime only uses the envs.\noptional obfuscation that was delivered ready and ",(0,r.jsx)(n.strong,{children:"deliberately not applied"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["Security does not come from hiding the ",(0,r.jsx)(n.code,{children:"anon"})," key \u2014 it is public by design, although the current\nclient does not receive it. It comes from the ",(0,r.jsx)(n.em,{children:"policies"}),", from the per-column grants and from the rate limit counted\nin Postgres (",(0,r.jsx)(n.code,{children:"src/lib/ratelimit.ts"})," + RPC ",(0,r.jsx)(n.code,{children:"rl_take"}),"), not in lambda memory."]}),"\n",(0,r.jsxs)(n.p,{children:["Player identity uses a stable UID to select the account and a token to authenticate\nthe session; the nickname is display data. Old clients and a database pending the\nprivate migration retain a temporary ",(0,r.jsx)(n.code,{children:"nickname + token"})," fallback."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"The ranking is off today"})," (",(0,r.jsx)(n.code,{children:"RANKING_ON"})," in ",(0,r.jsx)(n.code,{children:"src/lib/site.ts"}),") and was replaced by\nanonymous telemetry. It is a flag, not a removal \u2014 details in ",(0,r.jsx)(n.a,{href:"./status",children:"Current state"}),"."]}),"\n",(0,r.jsx)(n.admonition,{title:"None of this is required to run the game",type:"note",children:(0,r.jsxs)(n.p,{children:["Without the Supabase variables the site boots the same: the ranking routes respond\n",(0,r.jsx)(n.code,{children:"503 not_configured"})," and the pages show the notice. The game in ",(0,r.jsx)(n.code,{children:"public/"})," ",(0,r.jsx)(n.strong,{children:"uses none of\nthem"}),". See ",(0,r.jsx)(n.code,{children:".env.example"}),"."]})}),"\n",(0,r.jsx)(n.h2,{id:"asset-generation",children:"Asset generation \u2014 what is AI-generated, and by which service"}),"\n",(0,r.jsxs)(n.p,{children:["Almost every 3D and 2D asset in this game is ",(0,r.jsx)(n.strong,{children:"generated"}),", not hand-modeled. The real flow, not the\nhypothetical one:"]}),"\n","\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Service"}),(0,r.jsx)(n.th,{children:"What it generates"}),(0,r.jsx)(n.th,{children:"Script"}),(0,r.jsx)(n.th,{children:"Key"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"mint.gg"})," (Mint MCP)"]}),(0,r.jsx)(n.td,{children:"rigged characters, packs, animation"}),(0,r.jsxs)(n.td,{children:["MCP tools; ",(0,r.jsx)(n.code,{children:"mint-assets.json"})," records the result"]}),(0,r.jsx)(n.td,{children:"owner account via MCP"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Tripo3D"})}),(0,r.jsx)(n.td,{children:"3D props from text"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"tools/gen-asset.mjs --provider tripo"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"TRIPO_API_KEY"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Meshy"})}),(0,r.jsx)(n.td,{children:"3D props and rigging"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"tools/gen-asset.mjs --provider meshy"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MESHY_API_KEY"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"OpenRouter"})}),(0,r.jsx)(n.td,{children:"2D art"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"tools/gen-image.mjs"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"OPENROUTER_API_KEY"})})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"mint-assets.json"})," records ",(0,r.jsx)(n.strong,{children:"7 assets"})," generated through Mint (3 ",(0,r.jsx)(n.code,{children:"mint-model"})," \xb7 4 ",(0,r.jsx)(n.code,{children:"mint-asset-pack"}),")."]}),"\n",(0,r.jsxs)(n.p,{children:["API keys live in the gitignored root ",(0,r.jsx)(n.code,{children:".env"}),"; generation is offline and the game runs without them."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Block generated by ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(n.code,{children:"git grep -l SDK -- tools/ | grep .mjs \xb7 mint-assets.json"})]}),"\n"]}),"\n","\n",(0,r.jsx)(n.h3,{id:"characters-mintgg",children:"Characters: mint.gg"}),"\n",(0,r.jsxs)(n.p,{children:["The playable characters are rigged GLBs generated by ",(0,r.jsx)(n.strong,{children:"Mint"})," (mint.gg), through the MCP\ntools \u2014 ",(0,r.jsx)(n.code,{children:"start_model_generation"})," with ",(0,r.jsx)(n.code,{children:"riggable_character"})," in T-pose and empty hands, then\n",(0,r.jsx)(n.code,{children:"animate_generated_model"})," to come out with a skeleton."]}),"\n",(0,r.jsx)(n.p,{children:"Two non-obvious facts that save money and rounds:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Mint's base model does not come rigged."})," The skeleton only appears at the animation step.\nThe cheap path for a new character is: generate the base \u2192 rig it with ",(0,r.jsx)(n.strong,{children:"one"})," clip \u2192\nuse its ",(0,r.jsx)(n.code,{children:"rigged_character_glb"})," \u2192 reuse the shared clips."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Meshy rigs share the same bone names"})," (",(0,r.jsx)(n.code,{children:"Hips"}),", ",(0,r.jsx)(n.code,{children:"Spine"}),", ",(0,r.jsx)(n.code,{children:"Head"}),",\n",(0,r.jsx)(n.code,{children:"RightHand"}),"\u2026), so a clip pack generated once matches by name on any rig in the\nfamily. That is why ",(0,r.jsx)(n.code,{children:"public/models/anims/"})," has shared clips and per-character clips\nat the same time, and why the ",(0,r.jsx)(n.code,{children:"index.json"})," manifest exists (",(0,r.jsx)(n.code,{children:"npm run anims"}),") \u2014 without it the\ngame requested clips from characters that do not have them and filled the console with 404s."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"mint-assets.json"})," is the record of what was generated: ",(0,r.jsx)(n.code,{children:"assetId"}),", ",(0,r.jsx)(n.code,{children:"chatUrl"})," and a note on what\nwent wrong in the previous attempt. ",(0,r.jsx)(n.strong,{children:"Without that record there is no reviewing and no regenerating"})," \u2014\nthe asset becomes a binary with no provenance sitting in the repository."]}),"\n",(0,r.jsx)(n.h3,{id:"props-3d",children:"3D props: Tripo3D and Meshy"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"tools/gen-asset.mjs"})," generates a prop from text, downloads the GLB and writes it ",(0,r.jsx)(n.strong,{children:"already optimized"})," into\n",(0,r.jsx)(n.code,{children:"public/models/props/"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'node tools/gen-asset.mjs --prompt "caixa de som de baile" --id caixa_som\nnode tools/gen-asset.mjs --provider meshy --prompt "carro tunado" --id carro_tunado\nnode tools/gen-asset.mjs --resume --id caixa_som # task already paid for\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Tripo is the default; Meshy is the alternative. ",(0,r.jsx)(n.code,{children:"--face-limit"})," (default 12000), ",(0,r.jsx)(n.code,{children:"--raw-only"})," to\nskip optimization and ",(0,r.jsx)(n.code,{children:"--timeout"})," complete the options."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Maps do not need this."})," The registered maps are procedural geometry in Three.js \u2014\nstreet, shack, alley, sidewalk and roundabout are box and plane, which is what ",(0,r.jsx)(n.code,{children:"map_*.js"})," already does. What\ncomes from GLB are ",(0,r.jsx)(n.strong,{children:"props"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"art-2d-openrouter",children:"2D art: OpenRouter"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"tools/gen-image.mjs"})," is the 2D sibling: it generates faction posters, wallpapers and splash art from text\n(+ reference images), and delivers the file ",(0,r.jsx)(n.strong,{children:"already framed and compressed"})," for the box\nthe screen will draw it in."]}),"\n",(0,r.jsxs)(n.p,{children:["The cropping lives in the script on purpose. A faction board is a ",(0,r.jsx)(n.code,{children:"245\xd7620"})," box with\n",(0,r.jsx)(n.code,{children:"background-size: cover"}),"; landscape art enters it showing ~26% of its width \u2014 that is\nhow four cast posters became four portraits of ONE character. The generator\ndoes not offer that aspect ratio, so whoever publishes is who settles the bill: generate at the closest\naspect and center-crop down to the box's ",(0,r.jsx)(n.strong,{children:"real"})," ratio. That way what you look at before\ncommitting is byte for byte what the player sees."]}),"\n",(0,r.jsx)(n.h3,{id:"the-keys",children:"The keys"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"TRIPO_API_KEY"}),", ",(0,r.jsx)(n.code,{children:"MESHY_API_KEY"})," and ",(0,r.jsx)(n.code,{children:"OPENROUTER_API_KEY"})," are read from a ",(0,r.jsx)(n.code,{children:".env"})," at the root \u2014\n",(0,r.jsx)(n.strong,{children:"gitignored, mode 600"}),". Three rules the two scripts share, each with a\nreason:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:["The key never comes from ",(0,r.jsx)(n.code,{children:"argv"}),"."]})," A command-line argument leaks in the ",(0,r.jsx)(n.code,{children:"ps"})," of any\nprocess on the machine."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:["The ",(0,r.jsx)(n.code,{children:"Authorization"})," header only goes out to the API's own host."]})," The finished GLB comes from a\nthird-party CDN (signed link); sending the key along on the download would hand the credential\nto a host that is not the provider's. There is an allowlist, and ",(0,r.jsx)(n.code,{children:"redirect: 'error'"})," prevents a\n3xx from carrying the header to another domain."]}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsxs)(n.strong,{children:["Nothing is printed without going through ",(0,r.jsx)(n.code,{children:"redact()"}),"."]})}),"\n"]}),"\n",(0,r.jsxs)(n.admonition,{type:"caution",children:[(0,r.jsxs)(n.mdxAdmonitionTitle,{children:["These three keys are not in ",(0,r.jsx)(n.code,{children:".env.example"})]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:".env.example"})," covers only Supabase and the audio bundle. The asset-generation keys\nexist only in the owner's ",(0,r.jsx)(n.code,{children:".env"}),". Anyone who clones and wants to generate assets needs to create them by hand\nwith the names above \u2014 it is documented here and in each script's header, not in the example."]})]}),"\n",(0,r.jsx)(n.h2,{id:"glb-optimization",children:"GLB optimization: gltf-transform and meshoptimizer"}),"\n",(0,r.jsxs)(n.p,{children:["Every GLB that enters the repository goes through ",(0,r.jsx)(n.code,{children:"@gltf-transform"})," (",(0,r.jsx)(n.code,{children:"dedup"}),", ",(0,r.jsx)(n.code,{children:"prune"}),",\n",(0,r.jsx)(n.code,{children:"textureCompress"})," with ",(0,r.jsx)(n.strong,{children:"sharp"})," to WebP) and, on the static path, through ",(0,r.jsx)(n.strong,{children:"meshoptimizer"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["The reason is a real ceiling: ",(0,r.jsx)(n.strong,{children:"250 MB on CrazyGames"}),". A raw character GLB comes in at 4-5 MB,\ndominated by 2K PNG texture \u2014 and the optimization is almost entirely texture, not mesh."]}),"\n",(0,r.jsxs)(n.p,{children:["The pipeline scripts live in ",(0,r.jsx)(n.code,{children:"tools/"}),": ",(0,r.jsx)(n.code,{children:"optimize-props.mjs"}),", ",(0,r.jsx)(n.code,{children:"optimize-static.mjs"}),",\n",(0,r.jsx)(n.code,{children:"optimize-fpvm.mjs"}),", ",(0,r.jsx)(n.code,{children:"optimize-tribos.mjs"}),", plus the rig ones (",(0,r.jsx)(n.code,{children:"rig-from-donor.mjs"}),",\n",(0,r.jsx)(n.code,{children:"reskin-glb.mjs"}),", ",(0,r.jsx)(n.code,{children:"retarget-glb.mjs"}),") and the inspection ones (",(0,r.jsx)(n.code,{children:"inspect-glb.mjs"}),",\n",(0,r.jsx)(n.code,{children:"inspect-anim.mjs"}),", ",(0,r.jsx)(n.code,{children:"bones.mjs"}),")."]}),"\n",(0,r.jsx)(n.h2,{id:"playwright",children:"Playwright \u2014 every harness that needs a browser"}),"\n",(0,r.jsxs)(n.p,{children:["A ruler (quality gate) that depends on ",(0,r.jsx)(n.strong,{children:"pixels"})," runs in Chromium via Playwright. That is the case of\n",(0,r.jsx)(n.code,{children:"tools/eval/*-capture.mjs"}),", ",(0,r.jsx)(n.code,{children:"telas-*.mjs"}),", ",(0,r.jsx)(n.code,{children:"select-inflate.mjs"}),", ",(0,r.jsx)(n.code,{children:"crash-watch.mjs"})," and\n",(0,r.jsx)(n.code,{children:"fv-verify.mjs"}),", among others."]}),"\n",(0,r.jsx)(n.p,{children:"Two things you need to know before running any of them:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"It is expensive."})," Software rendering (SwiftShader) runs the game at ~0,3 FPS; an in-game\ncapture costs minutes per map/aspect. That cost is exactly what pushed the gate\nto pure node \u2014 and it is why the pixel invariants (",(0,r.jsx)(n.code,{children:"PX1"}),"\u2013",(0,r.jsx)(n.code,{children:"PX4"}),") are\n",(0,r.jsx)(n.strong,{children:"skipped"}),", with the reason stated."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"One session at a time."}),' Two headless captures in parallel take down the boot and produce a\n"frozen countdown" that looks like a bug and is load. A single agent runs the browser.']}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Some harnesses need the server up: ",(0,r.jsx)(n.code,{children:"npm run eval:serve &"})," first."]}),"\n",(0,r.jsx)(n.h2,{id:"agent-skills",children:"Agent skills"}),"\n",(0,r.jsxs)(n.p,{children:["This repository versions ",(0,r.jsx)(n.strong,{children:"skills"})," \u2014 packaged instructions an agent loads before\nworking. They live in ",(0,r.jsx)(n.code,{children:".agents/skills/"}),", and ",(0,r.jsx)(n.code,{children:".claude/skills/"})," are symlinks to there."]}),"\n","\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Count"}),(0,r.jsx)(n.th,{style:{textAlign:"right"},children:"How many"}),(0,r.jsx)(n.th,{children:"Meaning"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Declared in ",(0,r.jsx)(n.code,{children:"skills-lock.json"})]}),(0,r.jsx)(n.td,{style:{textAlign:"right"},children:"39"}),(0,r.jsx)(n.td,{children:"third-party skills pinned by source and hash"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Versioned"}),(0,r.jsx)(n.td,{style:{textAlign:"right"},children:"10"}),(0,r.jsx)(n.td,{children:"content available in a clean clone"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Versioned with ",(0,r.jsx)(n.code,{children:"SKILL.md"})]}),(0,r.jsx)(n.td,{style:{textAlign:"right"},children:"10"}),(0,r.jsx)(n.td,{children:"directly readable instructions"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"The counts differ by design: the lock records more third-party skills than the repository vendors."}),"\n",(0,r.jsxs)(n.p,{children:["The house workflow skill, ",(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"gauntlet-fps"})}),", is present locally and does not belong to the third-party lock."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Block generated by ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(n.code,{children:"git ls-files .agents/skills \xb7 skills-lock.json"})]}),"\n"]}),"\n","\n",(0,r.jsx)(n.p,{children:"The vast majority are third-party and cover Three.js (materials, lighting, shaders,\npost-processing, glTF loading, geometry, animation) and game design. They are\noptional context: nothing in the game depends on them."}),"\n",(0,r.jsx)(n.h3,{id:"the-gauntlet-loop",children:"The gauntlet loop"}),"\n",(0,r.jsxs)(n.p,{children:["The skill that is ",(0,r.jsx)(n.strong,{children:"not"})," third-party is ",(0,r.jsx)(n.code,{children:"gauntlet-fps"}),", and it encodes this house's work\ncycle:"]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"adversarial critic \u2192 builders in parallel \u2192 measured capture \u2192 A/B verification \u2192\nregression hunter"})}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"When to use:"}),' improving, evaluating or reviewing any part of the game \u2014 graphics,\nmap fidelity, weapon feel, menu, HUD, bots, movement \u2014 and when something is reported\nas ugly, weird or "doesn\'t look professional". ',(0,r.jsx)(n.strong,{children:"When not to use:"})," a one-line mechanical\ntask, or a conceptual question that does not touch the game."]}),"\n",(0,r.jsxs)(n.p,{children:["The whole cycle \u2014 the three rules, the problem each one solves and each one's measured\ncase \u2014 has its own page: ",(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.a,{href:"/docs/en/ai-instrumentation",children:"AI instrumentation"})}),". This section\nexists only to say that the skill exists and when to trigger it."]}),"\n",(0,r.jsx)(n.h2,{id:"the-documentation",children:"The documentation"}),"\n",(0,r.jsxs)(n.p,{children:["This doc is a ",(0,r.jsx)(n.strong,{children:"separate Docusaurus"}),", in ",(0,r.jsx)(n.code,{children:"docs/"}),", with its own ",(0,r.jsx)(n.code,{children:"package.json"})," and its\nown ",(0,r.jsx)(n.code,{children:"node_modules"}),". Nothing here is imported by the game or by the site."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"cd docs && npm install && npm start # http://localhost:3000/docs/\ncd docs && npm run build # docs/build/\ncd docs && npm run build:site # builds INTO public/docs/\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"baseUrl"})," is ",(0,r.jsx)(n.code,{children:"/docs/"})," because the output can be built into ",(0,r.jsx)(n.code,{children:"public/docs/"}),", and Astro copies\nall of ",(0,r.jsx)(n.code,{children:"public/"})," into ",(0,r.jsx)(n.code,{children:"dist/client/"}),"."]}),"\n",(0,r.jsx)(n.admonition,{title:"Every number on this page is generated",type:"tip",children:(0,r.jsxs)(n.p,{children:["The tables above come from ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"})," and are checked by\n",(0,r.jsx)(n.code,{children:"npm run docs:check"}),", inside ",(0,r.jsx)(n.code,{children:"check:fast"}),". The mechanism \u2014 what goes into a generated block, what\nstays hand-written, and how to paste a new block \u2014 is in\n",(0,r.jsx)(n.a,{href:"/docs/en/architecture#generated-vs-not",children:"Architecture"}),"."]})})]})}function l(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},8453(e,n,s){s.d(n,{R:()=>d,x:()=>o});var t=s(6540);const r={},i=t.createContext(r);function d(e){const n=t.useContext(i);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),t.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkcoro_solto_docs||=[]).push([[502],{8909(e,n,s){s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>l,frontMatter:()=>d,metadata:()=>t,toc:()=>h});const t=JSON.parse('{"id":"stack","title":"Stack and tools","description":"Three.js/WebGL with no build, Astro on Vercel, Supabase, the asset generation pipeline (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform and the agent skills \u2014 each with the version read from package.json.","source":"@site/i18n/en/docusaurus-plugin-content-docs/current/stack.md","sourceDirName":".","slug":"/stack","permalink":"/docs/en/stack","draft":false,"unlisted":false,"editUrl":"https://github.com/corosolto/client/tree/main/docs/docs/stack.md","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"id":"stack","title":"Stack and tools","sidebar_label":"Stack and tools","sidebar_position":2,"slug":"/stack","description":"Three.js/WebGL with no build, Astro on Vercel, Supabase, the asset generation pipeline (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform and the agent skills \u2014 each with the version read from package.json."},"sidebar":"dev","previous":{"title":"Getting started","permalink":"/docs/en/"},"next":{"title":"AI instrumentation","permalink":"/docs/en/ai-instrumentation"}}');var r=s(4848),i=s(8453);const d={id:"stack",title:"Stack and tools",sidebar_label:"Stack and tools",sidebar_position:2,slug:"/stack",description:"Three.js/WebGL with no build, Astro on Vercel, Supabase, the asset generation pipeline (mint.gg, Tripo3D, Meshy, OpenRouter), Playwright, gltf-transform and the agent skills \u2014 each with the version read from package.json."},o="Stack and tools",c={},h=[{value:"The two zones, and why the boundary is hard",id:"the-two-zones",level:2},{value:"public/ \u2014 the GAME: Three.js, WebGL, zero build",id:"public-the-game",level:3},{value:"src/ \u2014 the SITE: Astro with SSR on Vercel",id:"src-the-site",level:3},{value:"Database \u2014 managed Postgres, RLS and telemetry",id:"database",level:3},{value:"Asset generation \u2014 what is AI-generated, and by which service",id:"asset-generation",level:2},{value:"Characters: mint.gg",id:"characters-mintgg",level:3},{value:"3D props: Tripo3D and Meshy",id:"props-3d",level:3},{value:"2D art: OpenRouter",id:"art-2d-openrouter",level:3},{value:"The keys",id:"the-keys",level:3},{value:"GLB optimization: gltf-transform and meshoptimizer",id:"glb-optimization",level:2},{value:"Playwright \u2014 every harness that needs a browser",id:"playwright",level:2},{value:"Agent skills",id:"agent-skills",level:2},{value:"The gauntlet loop",id:"the-gauntlet-loop",level:3},{value:"The documentation",id:"the-documentation",level:2}];function a(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:["\n",(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"stack-and-tools",children:"Stack and tools"})}),"\n",(0,r.jsxs)(n.p,{children:["This page answers the question ",(0,r.jsx)(n.em,{children:'"what is this made with?"'})," \u2014 and it answers with the ",(0,r.jsx)(n.strong,{children:"declared\nversion"}),", not the remembered one. The table below is generated by ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),"\nfrom ",(0,r.jsx)(n.code,{children:"package.json"}),", ",(0,r.jsx)(n.code,{children:"docs/package.json"})," and the vendored Three.js itself."]}),"\n","\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Layer"}),(0,r.jsx)(n.th,{children:"Tool"}),(0,r.jsx)(n.th,{children:"Version"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"3D engine (WebGL)"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"Three.js"}),", vendored"]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"r160"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Game"}),(0,r.jsxs)(n.td,{children:["vanilla ES modules, ",(0,r.jsx)(n.strong,{children:"zero build"})]}),(0,r.jsx)(n.td,{children:"44 files"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Site"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"Astro"})," with SSR"]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^7.1.1"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Hosting"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"Vercel"})," adapter"]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^11.0.6"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Database"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"managed Postgres"})," (RLS; private schema)"]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^2.110.7"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Browser checks"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Playwright"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^1.62.1"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GLB pipeline"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"gltf-transform"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^4.4.1"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Mesh compression"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"meshoptimizer"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^1.2.0"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Images"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"sharp"})," \xb7 ",(0,r.jsx)(n.strong,{children:"resvg"})]}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"^0.35.3"})," \xb7 ",(0,r.jsx)(n.code,{children:"^2.6.2"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"This documentation"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Docusaurus"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"3.6.3"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"CI runtime"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Node"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"22"})})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["Three.js comes from ",(0,r.jsx)(n.code,{children:"public/vendor/three.module.js"}),". Of the scripts in ",(0,r.jsx)(n.code,{children:"tools/"}),", ",(0,r.jsx)(n.strong,{children:"110"})," import Playwright, ",(0,r.jsx)(n.strong,{children:"37"})," import gltf-transform, and ",(0,r.jsx)(n.strong,{children:"4"})," import meshoptimizer."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Block generated by ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(n.code,{children:"dependencies/devDependencies do package.json \xb7 REVISION de public/vendor/three.module.js"})]}),"\n"]}),"\n","\n",(0,r.jsx)(n.h2,{id:"the-two-zones",children:"The two zones, and why the boundary is hard"}),"\n",(0,r.jsxs)(n.p,{children:["The repository has ",(0,r.jsx)(n.strong,{children:"two applications with opposite rules"}),", and almost every misunderstanding from\nnewcomers is born from treating them as one."]}),"\n",(0,r.jsxs)(n.h3,{id:"public-the-game",children:[(0,r.jsx)(n.code,{children:"public/"})," \u2014 the GAME: Three.js, WebGL, zero build"]}),"\n",(0,r.jsxs)(n.p,{children:["The game is ",(0,r.jsx)(n.strong,{children:"vanilla JavaScript with ES modules served raw"}),". There is no bundler, no\ntranspiler, no build step. The browser downloads ",(0,r.jsx)(n.code,{children:"public/js/game.js"})," exactly as it is in the\nrepository."]}),"\n",(0,r.jsxs)(n.p,{children:["This is a ",(0,r.jsx)(n.strong,{children:"design decision, not laziness"}),", and it pays off in three places:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"The game runs by dragging the folder onto any static host."})," It does not depend on Astro,\ndoes not depend on Vercel, does not depend on npm at runtime. It is what makes it viable to ship on a\nportal (CrazyGames, itch) without rewriting anything."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:["The harness can boot the ",(0,r.jsx)(n.code,{children:"Game"})," class in pure node."]})," ",(0,r.jsx)(n.code,{children:"tools/eval/harness.mjs"}),"\nimports the ",(0,r.jsx)(n.strong,{children:"production code"})," with DOM and canvas stubbed, and measures the real game in\nseconds. A bundler in the middle would break that \u2014 and without that there is no gate."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:[(0,r.jsx)(n.code,{children:"node --check"})," on each file is a complete syntax test"]})," (",(0,r.jsx)(n.code,{children:"npm run syntax"}),"),\nbecause the file node parses is byte for byte what the browser executes."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["The price, which is also real: ",(0,r.jsx)(n.strong,{children:"cache"}),". Without a build there is no hash in the filename, so\ninvalidation is manual \u2014 the import map's ",(0,r.jsx)(n.code,{children:"?v="}),". The rule and what it has already cost are in\n",(0,r.jsx)(n.a,{href:"/docs/en/#the-two-zones",children:"Getting started"}),", in one place only."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Three.js is vendored"})," at ",(0,r.jsx)(n.code,{children:"public/vendor/three.module.js"})," (plus ",(0,r.jsx)(n.code,{children:"vendor/addons/"}),").\nNo CDN and no runtime dependency: the import map points to the local file. Do not\nadd a CDN or a runtime package without opening an issue."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"WebGL is the target, and a weak machine is a requirement."})," There is a ",(0,r.jsx)(n.code,{children:"quality: 'low'"})," path with no\npost-processing and a querystring kill-switch for every risky change (",(0,r.jsx)(n.code,{children:"?bloom=0"}),",\n",(0,r.jsx)(n.code,{children:"?ao=0"}),", ",(0,r.jsx)(n.code,{children:"?fxaa=0"}),", ",(0,r.jsx)(n.code,{children:"?water=0"}),"). Every graphics change that requires extra rendering has to\ndeclare its measured cost."]}),"\n",(0,r.jsxs)(n.h3,{id:"src-the-site",children:[(0,r.jsx)(n.code,{children:"src/"})," \u2014 the SITE: Astro with SSR on Vercel"]}),"\n",(0,r.jsxs)(n.p,{children:["The site is ",(0,r.jsx)(n.a,{href:"https://astro.build",children:"Astro"})," with the Vercel adapter. ",(0,r.jsx)(n.code,{children:"astro.config.mjs"})," is set to\n",(0,r.jsx)(n.code,{children:"output: 'static'"})," ",(0,r.jsx)(n.strong,{children:"with adapter"}),", and the routes that need a server opt in with\n",(0,r.jsx)(n.code,{children:"export const prerender = false"})," one by one \u2014 the case of ",(0,r.jsx)(n.code,{children:"/ranking"}),", ",(0,r.jsx)(n.code,{children:"/u/*"}),",\n",(0,r.jsx)(n.code,{children:"/sitemap.xml"})," and all the ",(0,r.jsx)(n.code,{children:"/api/*"})," routes."]}),"\n",(0,r.jsx)(n.p,{children:"Here a framework is welcome. The rules that apply:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["the Supabase ",(0,r.jsx)(n.code,{children:"service_role"})," lives ",(0,r.jsx)(n.strong,{children:"server-side only"})," and never reaches the browser;"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"site"})," in ",(0,r.jsx)(n.code,{children:"astro.config.mjs"})," is ",(0,r.jsxs)(n.strong,{children:["with ",(0,r.jsx)(n.code,{children:"www"})]}),", and every canonical comes from there;"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"vercel.json"})," carries the security headers (CSP, HSTS, nosniff, Referrer-Policy,\nPermissions-Policy) and the CDN cache."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["And the gotcha that costs everyone their first hour: ",(0,r.jsxs)(n.strong,{children:[(0,r.jsx)(n.code,{children:"src/pages/index.astro"})," IS the\ngame"]}),", served at the ",(0,r.jsx)(n.code,{children:"/"})," route. There is no ",(0,r.jsx)(n.code,{children:"public/index.html"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"database",children:"Database \u2014 managed Postgres, RLS and telemetry"}),"\n",(0,r.jsxs)(n.p,{children:["The ranking and the telemetry live in a managed Postgres. Schema and migrations are\nprivate (outside the repo \u2014 a security decision); the runtime only uses the envs.\noptional obfuscation that was delivered ready and ",(0,r.jsx)(n.strong,{children:"deliberately not applied"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["Security does not come from hiding the ",(0,r.jsx)(n.code,{children:"anon"})," key \u2014 it is public by design, although the current\nclient does not receive it. It comes from the ",(0,r.jsx)(n.em,{children:"policies"}),", from the per-column grants and from the rate limit counted\nin Postgres (",(0,r.jsx)(n.code,{children:"src/lib/ratelimit.ts"})," + RPC ",(0,r.jsx)(n.code,{children:"rl_take"}),"), not in lambda memory."]}),"\n",(0,r.jsxs)(n.p,{children:["Player identity uses a stable UID to select the account and a token to authenticate\nthe session; the nickname is display data. Old clients and a database pending the\nprivate migration retain a temporary ",(0,r.jsx)(n.code,{children:"nickname + token"})," fallback."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"The ranking is off today"})," (",(0,r.jsx)(n.code,{children:"RANKING_ON"})," in ",(0,r.jsx)(n.code,{children:"src/lib/site.ts"}),") and was replaced by\nanonymous telemetry. It is a flag, not a removal \u2014 details in ",(0,r.jsx)(n.a,{href:"./status",children:"Current state"}),"."]}),"\n",(0,r.jsx)(n.admonition,{title:"None of this is required to run the game",type:"note",children:(0,r.jsxs)(n.p,{children:["Without the Supabase variables the site boots the same: the ranking routes respond\n",(0,r.jsx)(n.code,{children:"503 not_configured"})," and the pages show the notice. The game in ",(0,r.jsx)(n.code,{children:"public/"})," ",(0,r.jsx)(n.strong,{children:"uses none of\nthem"}),". See ",(0,r.jsx)(n.code,{children:".env.example"}),"."]})}),"\n",(0,r.jsx)(n.h2,{id:"asset-generation",children:"Asset generation \u2014 what is AI-generated, and by which service"}),"\n",(0,r.jsxs)(n.p,{children:["Almost every 3D and 2D asset in this game is ",(0,r.jsx)(n.strong,{children:"generated"}),", not hand-modeled. The real flow, not the\nhypothetical one:"]}),"\n","\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Service"}),(0,r.jsx)(n.th,{children:"What it generates"}),(0,r.jsx)(n.th,{children:"Script"}),(0,r.jsx)(n.th,{children:"Key"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"mint.gg"})," (Mint MCP)"]}),(0,r.jsx)(n.td,{children:"rigged characters, packs, animation"}),(0,r.jsxs)(n.td,{children:["MCP tools; ",(0,r.jsx)(n.code,{children:"mint-assets.json"})," records the result"]}),(0,r.jsx)(n.td,{children:"owner account via MCP"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Tripo3D"})}),(0,r.jsx)(n.td,{children:"3D props from text"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"tools/gen-asset.mjs --provider tripo"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"TRIPO_API_KEY"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Meshy"})}),(0,r.jsx)(n.td,{children:"3D props and rigging"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"tools/gen-asset.mjs --provider meshy"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MESHY_API_KEY"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"OpenRouter"})}),(0,r.jsx)(n.td,{children:"2D art"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"tools/gen-image.mjs"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"OPENROUTER_API_KEY"})})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"mint-assets.json"})," records ",(0,r.jsx)(n.strong,{children:"7 assets"})," generated through Mint (3 ",(0,r.jsx)(n.code,{children:"mint-model"})," \xb7 4 ",(0,r.jsx)(n.code,{children:"mint-asset-pack"}),")."]}),"\n",(0,r.jsxs)(n.p,{children:["API keys live in the gitignored root ",(0,r.jsx)(n.code,{children:".env"}),"; generation is offline and the game runs without them."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Block generated by ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(n.code,{children:"git grep -l SDK -- tools/ | grep .mjs \xb7 mint-assets.json"})]}),"\n"]}),"\n","\n",(0,r.jsx)(n.h3,{id:"characters-mintgg",children:"Characters: mint.gg"}),"\n",(0,r.jsxs)(n.p,{children:["The playable characters are rigged GLBs generated by ",(0,r.jsx)(n.strong,{children:"Mint"})," (mint.gg), through the MCP\ntools \u2014 ",(0,r.jsx)(n.code,{children:"start_model_generation"})," with ",(0,r.jsx)(n.code,{children:"riggable_character"})," in T-pose and empty hands, then\n",(0,r.jsx)(n.code,{children:"animate_generated_model"})," to come out with a skeleton."]}),"\n",(0,r.jsx)(n.p,{children:"Two non-obvious facts that save money and rounds:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Mint's base model does not come rigged."})," The skeleton only appears at the animation step.\nThe cheap path for a new character is: generate the base \u2192 rig it with ",(0,r.jsx)(n.strong,{children:"one"})," clip \u2192\nuse its ",(0,r.jsx)(n.code,{children:"rigged_character_glb"})," \u2192 reuse the shared clips."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Meshy rigs share the same bone names"})," (",(0,r.jsx)(n.code,{children:"Hips"}),", ",(0,r.jsx)(n.code,{children:"Spine"}),", ",(0,r.jsx)(n.code,{children:"Head"}),",\n",(0,r.jsx)(n.code,{children:"RightHand"}),"\u2026), so a clip pack generated once matches by name on any rig in the\nfamily. That is why ",(0,r.jsx)(n.code,{children:"public/models/anims/"})," has shared clips and per-character clips\nat the same time, and why the ",(0,r.jsx)(n.code,{children:"index.json"})," manifest exists (",(0,r.jsx)(n.code,{children:"npm run anims"}),") \u2014 without it the\ngame requested clips from characters that do not have them and filled the console with 404s."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"mint-assets.json"})," is the record of what was generated: ",(0,r.jsx)(n.code,{children:"assetId"}),", ",(0,r.jsx)(n.code,{children:"chatUrl"})," and a note on what\nwent wrong in the previous attempt. ",(0,r.jsx)(n.strong,{children:"Without that record there is no reviewing and no regenerating"})," \u2014\nthe asset becomes a binary with no provenance sitting in the repository."]}),"\n",(0,r.jsx)(n.h3,{id:"props-3d",children:"3D props: Tripo3D and Meshy"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"tools/gen-asset.mjs"})," generates a prop from text, downloads the GLB and writes it ",(0,r.jsx)(n.strong,{children:"already optimized"})," into\n",(0,r.jsx)(n.code,{children:"public/models/props/"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'node tools/gen-asset.mjs --prompt "caixa de som de baile" --id caixa_som\nnode tools/gen-asset.mjs --provider meshy --prompt "carro tunado" --id carro_tunado\nnode tools/gen-asset.mjs --resume --id caixa_som # task already paid for\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Tripo is the default; Meshy is the alternative. ",(0,r.jsx)(n.code,{children:"--face-limit"})," (default 12000), ",(0,r.jsx)(n.code,{children:"--raw-only"})," to\nskip optimization and ",(0,r.jsx)(n.code,{children:"--timeout"})," complete the options."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Maps do not need this."})," The registered maps are procedural geometry in Three.js \u2014\nstreet, shack, alley, sidewalk and roundabout are box and plane, which is what ",(0,r.jsx)(n.code,{children:"map_*.js"})," already does. What\ncomes from GLB are ",(0,r.jsx)(n.strong,{children:"props"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"art-2d-openrouter",children:"2D art: OpenRouter"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"tools/gen-image.mjs"})," is the 2D sibling: it generates faction posters, wallpapers and splash art from text\n(+ reference images), and delivers the file ",(0,r.jsx)(n.strong,{children:"already framed and compressed"})," for the box\nthe screen will draw it in."]}),"\n",(0,r.jsxs)(n.p,{children:["The cropping lives in the script on purpose. A faction board is a ",(0,r.jsx)(n.code,{children:"245\xd7620"})," box with\n",(0,r.jsx)(n.code,{children:"background-size: cover"}),"; landscape art enters it showing ~26% of its width \u2014 that is\nhow four cast posters became four portraits of ONE character. The generator\ndoes not offer that aspect ratio, so whoever publishes is who settles the bill: generate at the closest\naspect and center-crop down to the box's ",(0,r.jsx)(n.strong,{children:"real"})," ratio. That way what you look at before\ncommitting is byte for byte what the player sees."]}),"\n",(0,r.jsx)(n.h3,{id:"the-keys",children:"The keys"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"TRIPO_API_KEY"}),", ",(0,r.jsx)(n.code,{children:"MESHY_API_KEY"})," and ",(0,r.jsx)(n.code,{children:"OPENROUTER_API_KEY"})," are read from a ",(0,r.jsx)(n.code,{children:".env"})," at the root \u2014\n",(0,r.jsx)(n.strong,{children:"gitignored, mode 600"}),". Three rules the two scripts share, each with a\nreason:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:["The key never comes from ",(0,r.jsx)(n.code,{children:"argv"}),"."]})," A command-line argument leaks in the ",(0,r.jsx)(n.code,{children:"ps"})," of any\nprocess on the machine."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:["The ",(0,r.jsx)(n.code,{children:"Authorization"})," header only goes out to the API's own host."]})," The finished GLB comes from a\nthird-party CDN (signed link); sending the key along on the download would hand the credential\nto a host that is not the provider's. There is an allowlist, and ",(0,r.jsx)(n.code,{children:"redirect: 'error'"})," prevents a\n3xx from carrying the header to another domain."]}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsxs)(n.strong,{children:["Nothing is printed without going through ",(0,r.jsx)(n.code,{children:"redact()"}),"."]})}),"\n"]}),"\n",(0,r.jsxs)(n.admonition,{type:"caution",children:[(0,r.jsxs)(n.mdxAdmonitionTitle,{children:["These three keys are not in ",(0,r.jsx)(n.code,{children:".env.example"})]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:".env.example"})," covers only Supabase and the audio bundle. The asset-generation keys\nexist only in the owner's ",(0,r.jsx)(n.code,{children:".env"}),". Anyone who clones and wants to generate assets needs to create them by hand\nwith the names above \u2014 it is documented here and in each script's header, not in the example."]})]}),"\n",(0,r.jsx)(n.h2,{id:"glb-optimization",children:"GLB optimization: gltf-transform and meshoptimizer"}),"\n",(0,r.jsxs)(n.p,{children:["Every GLB that enters the repository goes through ",(0,r.jsx)(n.code,{children:"@gltf-transform"})," (",(0,r.jsx)(n.code,{children:"dedup"}),", ",(0,r.jsx)(n.code,{children:"prune"}),",\n",(0,r.jsx)(n.code,{children:"textureCompress"})," with ",(0,r.jsx)(n.strong,{children:"sharp"})," to WebP) and, on the static path, through ",(0,r.jsx)(n.strong,{children:"meshoptimizer"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["The reason is a real ceiling: ",(0,r.jsx)(n.strong,{children:"250 MB on CrazyGames"}),". A raw character GLB comes in at 4-5 MB,\ndominated by 2K PNG texture \u2014 and the optimization is almost entirely texture, not mesh."]}),"\n",(0,r.jsxs)(n.p,{children:["The pipeline scripts live in ",(0,r.jsx)(n.code,{children:"tools/"}),": ",(0,r.jsx)(n.code,{children:"optimize-props.mjs"}),", ",(0,r.jsx)(n.code,{children:"optimize-static.mjs"}),",\n",(0,r.jsx)(n.code,{children:"optimize-fpvm.mjs"}),", ",(0,r.jsx)(n.code,{children:"optimize-tribos.mjs"}),", plus the rig ones (",(0,r.jsx)(n.code,{children:"rig-from-donor.mjs"}),",\n",(0,r.jsx)(n.code,{children:"reskin-glb.mjs"}),", ",(0,r.jsx)(n.code,{children:"retarget-glb.mjs"}),") and the inspection ones (",(0,r.jsx)(n.code,{children:"inspect-glb.mjs"}),",\n",(0,r.jsx)(n.code,{children:"inspect-anim.mjs"}),", ",(0,r.jsx)(n.code,{children:"bones.mjs"}),")."]}),"\n",(0,r.jsx)(n.h2,{id:"playwright",children:"Playwright \u2014 every harness that needs a browser"}),"\n",(0,r.jsxs)(n.p,{children:["A ruler (quality gate) that depends on ",(0,r.jsx)(n.strong,{children:"pixels"})," runs in Chromium via Playwright. That is the case of\n",(0,r.jsx)(n.code,{children:"tools/eval/*-capture.mjs"}),", ",(0,r.jsx)(n.code,{children:"telas-*.mjs"}),", ",(0,r.jsx)(n.code,{children:"select-inflate.mjs"}),", ",(0,r.jsx)(n.code,{children:"crash-watch.mjs"})," and\n",(0,r.jsx)(n.code,{children:"fv-verify.mjs"}),", among others."]}),"\n",(0,r.jsx)(n.p,{children:"Two things you need to know before running any of them:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"It is expensive."})," Software rendering (SwiftShader) runs the game at ~0,3 FPS; an in-game\ncapture costs minutes per map/aspect. That cost is exactly what pushed the gate\nto pure node \u2014 and it is why the pixel invariants (",(0,r.jsx)(n.code,{children:"PX1"}),"\u2013",(0,r.jsx)(n.code,{children:"PX4"}),") are\n",(0,r.jsx)(n.strong,{children:"skipped"}),", with the reason stated."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"One session at a time."}),' Two headless captures in parallel take down the boot and produce a\n"frozen countdown" that looks like a bug and is load. A single agent runs the browser.']}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Some harnesses need the server up: ",(0,r.jsx)(n.code,{children:"npm run eval:serve &"})," first."]}),"\n",(0,r.jsx)(n.h2,{id:"agent-skills",children:"Agent skills"}),"\n",(0,r.jsxs)(n.p,{children:["This repository versions ",(0,r.jsx)(n.strong,{children:"skills"})," \u2014 packaged instructions an agent loads before\nworking. They live in ",(0,r.jsx)(n.code,{children:".agents/skills/"}),", and ",(0,r.jsx)(n.code,{children:".claude/skills/"})," are symlinks to there."]}),"\n","\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Count"}),(0,r.jsx)(n.th,{style:{textAlign:"right"},children:"How many"}),(0,r.jsx)(n.th,{children:"Meaning"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Declared in ",(0,r.jsx)(n.code,{children:"skills-lock.json"})]}),(0,r.jsx)(n.td,{style:{textAlign:"right"},children:"39"}),(0,r.jsx)(n.td,{children:"third-party skills pinned by source and hash"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Versioned"}),(0,r.jsx)(n.td,{style:{textAlign:"right"},children:"10"}),(0,r.jsx)(n.td,{children:"content available in a clean clone"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Versioned with ",(0,r.jsx)(n.code,{children:"SKILL.md"})]}),(0,r.jsx)(n.td,{style:{textAlign:"right"},children:"10"}),(0,r.jsx)(n.td,{children:"directly readable instructions"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"The counts differ by design: the lock records more third-party skills than the repository vendors."}),"\n",(0,r.jsxs)(n.p,{children:["The house workflow skill, ",(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"gauntlet-fps"})}),", is present locally and does not belong to the third-party lock."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Block generated by ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"}),". Source: ",(0,r.jsx)(n.code,{children:"git ls-files .agents/skills \xb7 skills-lock.json"})]}),"\n"]}),"\n","\n",(0,r.jsx)(n.p,{children:"The vast majority are third-party and cover Three.js (materials, lighting, shaders,\npost-processing, glTF loading, geometry, animation) and game design. They are\noptional context: nothing in the game depends on them."}),"\n",(0,r.jsx)(n.h3,{id:"the-gauntlet-loop",children:"The gauntlet loop"}),"\n",(0,r.jsxs)(n.p,{children:["The skill that is ",(0,r.jsx)(n.strong,{children:"not"})," third-party is ",(0,r.jsx)(n.code,{children:"gauntlet-fps"}),", and it encodes this house's work\ncycle:"]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"adversarial critic \u2192 builders in parallel \u2192 measured capture \u2192 A/B verification \u2192\nregression hunter"})}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"When to use:"}),' improving, evaluating or reviewing any part of the game \u2014 graphics,\nmap fidelity, weapon feel, menu, HUD, bots, movement \u2014 and when something is reported\nas ugly, weird or "doesn\'t look professional". ',(0,r.jsx)(n.strong,{children:"When not to use:"})," a one-line mechanical\ntask, or a conceptual question that does not touch the game."]}),"\n",(0,r.jsxs)(n.p,{children:["The whole cycle \u2014 the three rules, the problem each one solves and each one's measured\ncase \u2014 has its own page: ",(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.a,{href:"/docs/en/ai-instrumentation",children:"AI instrumentation"})}),". This section\nexists only to say that the skill exists and when to trigger it."]}),"\n",(0,r.jsx)(n.h2,{id:"the-documentation",children:"The documentation"}),"\n",(0,r.jsxs)(n.p,{children:["This doc is a ",(0,r.jsx)(n.strong,{children:"separate Docusaurus"}),", in ",(0,r.jsx)(n.code,{children:"docs/"}),", with its own ",(0,r.jsx)(n.code,{children:"package.json"})," and its\nown ",(0,r.jsx)(n.code,{children:"node_modules"}),". Nothing here is imported by the game or by the site."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"cd docs && npm install && npm start # http://localhost:3000/docs/\ncd docs && npm run build # docs/build/\ncd docs && npm run build:site # builds INTO public/docs/\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"baseUrl"})," is ",(0,r.jsx)(n.code,{children:"/docs/"})," because the output can be built into ",(0,r.jsx)(n.code,{children:"public/docs/"}),", and Astro copies\nall of ",(0,r.jsx)(n.code,{children:"public/"})," into ",(0,r.jsx)(n.code,{children:"dist/client/"}),"."]}),"\n",(0,r.jsx)(n.admonition,{title:"Every number on this page is generated",type:"tip",children:(0,r.jsxs)(n.p,{children:["The tables above come from ",(0,r.jsx)(n.code,{children:"node tools/gen-docs.mjs"})," and are checked by\n",(0,r.jsx)(n.code,{children:"npm run docs:check"}),", inside ",(0,r.jsx)(n.code,{children:"check:fast"}),". The mechanism \u2014 what goes into a generated block, what\nstays hand-written, and how to paste a new block \u2014 is in\n",(0,r.jsx)(n.a,{href:"/docs/en/architecture#generated-vs-not",children:"Architecture"}),"."]})})]})}function l(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},8453(e,n,s){s.d(n,{R:()=>d,x:()=>o});var t=s(6540);const r={},i=t.createContext(r);function d(e){const n=t.useContext(i);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),t.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/public/docs/en/assets/js/runtime~main.6bb9aa27.js b/public/docs/en/assets/js/runtime~main.acec5d99.js similarity index 62% rename from public/docs/en/assets/js/runtime~main.6bb9aa27.js rename to public/docs/en/assets/js/runtime~main.acec5d99.js index d6e986e2b..aeadffea9 100644 --- a/public/docs/en/assets/js/runtime~main.6bb9aa27.js +++ b/public/docs/en/assets/js/runtime~main.acec5d99.js @@ -1 +1 @@ -(()=>{"use strict";var e={};const t={};function r(o){const n=t[o];if(void 0!==n)return n.exports;const c=t[o]={exports:{}};return e[o].call(c.exports,c,c.exports,r),c.exports}r.m=e,(()=>{const e=[];r.O=(t,o,n,c)=>{if(o){c=c||0;for(var a=e.length;a>0&&e[a-1][2]>c;a--)e[a]=e[a-1];return void(e[a]=[o,n,c])}let s=1/0;for(a=0;a=c)&&Object.keys(r.O).every(e=>r.O[e](o[f]))?o.splice(f--,1):(i=!1,c{const t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},(()=>{const e=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;let t;r.t=function(o,n){if(1&n&&(o=this(o)),8&n)return o;if("object"==typeof o&&o){if(4&n&&o.__esModule)return o;if(16&n&&"function"==typeof o.then)return o}const c=Object.create(null);r.r(c);const a={};t=t||[null,e({}),e([]),e(e)];for(var s=2&n&&o;("object"==typeof s||"function"==typeof s)&&!~t.indexOf(s);s=e(s))Object.getOwnPropertyNames(s).forEach(e=>a[e]=()=>o[e]);return a.default=()=>o,r.d(c,a),c}})(),r.d=(e,t)=>{if(Array.isArray(t))for(var o=0;oPromise.all(Object.keys(r.f).reduce((t,o)=>(r.f[o](e,t),t),[])),r.u=e=>"assets/js/"+({3:"7cf461fa",48:"a94703ab",98:"a7bd4aaa",305:"dc729c33",401:"17896441",502:"ed1a65a0",515:"a1985327",566:"3c097e0f",571:"c0b876b6",609:"fc54c9f2",647:"5e95c892",671:"dbd2121f",703:"202fc7b2",742:"aba21aa0"}[e]||e)+"."+{3:"6b3dd629",48:"c13f04ba",98:"2eacc047",237:"7f505004",305:"12b1db9a",401:"02cdecdc",502:"28d4867f",515:"b0b84147",566:"29dc8546",571:"6d9b0018",609:"1d19854c",647:"00e4eac4",671:"11849500",703:"fc87f7c1",742:"a2338f79"}[e]+".js",r.miniCssF=e=>{},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{const e={},t="coro-solto-docs:";r.l=(o,n,c,a)=>{if(e[o])return void e[o].push(n);let s,f;if(void 0!==c){const e=document.getElementsByTagName("script");for(var i=0;i{s.onerror=s.onload=null,clearTimeout(u);const n=e[o];if(delete e[o],s.parentNode?.removeChild(s),n?.forEach(e=>e(r)),t)return t(r)},u=setTimeout(l.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=l.bind(null,s.onerror),s.onload=l.bind(null,s.onload),f&&document.head.appendChild(s)}})(),r.r=e=>{Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.dn=e=>{var t=Object.getOwnPropertyDescriptor(e,"name");(!t||!t.writable&&t.configurable)&&Object.defineProperty(e,"name",{value:"default",configurable:!0})},r.cjs=e=>{const t={exports:{}};return e.call(t.exports,t,t.exports),t.exports},r.p="/docs/en/",r.gca=function(e){return e={17896441:"401","7cf461fa":"3",a94703ab:"48",a7bd4aaa:"98",dc729c33:"305",ed1a65a0:"502",a1985327:"515","3c097e0f":"566",c0b876b6:"571",fc54c9f2:"609","5e95c892":"647",dbd2121f:"671","202fc7b2":"703",aba21aa0:"742"}[e]||e,r.p+r.u(e)},(()=>{const e={354:0,869:0};r.f.j=(t,o)=>{let n=r.o(e,t)?e[t]:void 0;if(0!==n)if(n)o.push(n[2]);else if(/^(354|869)$/.test(t))e[t]=0;else{const c=new Promise((r,o)=>n=e[t]=[r,o]);o.push(n[2]=c);const a=r.p+r.u(t),s=new Error,f=o=>{if(r.o(e,t)&&(n=e[t],0!==n&&(e[t]=void 0),n)){const e=o&&("load"===o.type?"missing":o.type),r=o&&o.target&&o.target.src;s.message="Loading chunk "+t+" failed.\n("+e+": "+r+")",s.name="ChunkLoadError",s.type=e,s.request=r,s.event=o,n[1](s)}};r.l(a,f,"chunk-"+t,t)}},r.O.j=t=>0===e[t];const t=(t,o)=>{let[n,c,a]=o;var s,f,i=0;if(n.some(t=>0!==e[t])){for(s in c)r.o(c,s)&&(r.m[s]=c[s]);if(a)var l=a(r)}for(t&&t(o);i{"use strict";var e={};const t={};function r(o){const n=t[o];if(void 0!==n)return n.exports;const a=t[o]={exports:{}};return e[o].call(a.exports,a,a.exports,r),a.exports}r.m=e,(()=>{const e=[];r.O=(t,o,n,a)=>{if(o){a=a||0;for(var c=e.length;c>0&&e[c-1][2]>a;c--)e[c]=e[c-1];return void(e[c]=[o,n,a])}let s=1/0;for(c=0;c=a)&&Object.keys(r.O).every(e=>r.O[e](o[f]))?o.splice(f--,1):(i=!1,a{const t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},(()=>{const e=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;let t;r.t=function(o,n){if(1&n&&(o=this(o)),8&n)return o;if("object"==typeof o&&o){if(4&n&&o.__esModule)return o;if(16&n&&"function"==typeof o.then)return o}const a=Object.create(null);r.r(a);const c={};t=t||[null,e({}),e([]),e(e)];for(var s=2&n&&o;("object"==typeof s||"function"==typeof s)&&!~t.indexOf(s);s=e(s))Object.getOwnPropertyNames(s).forEach(e=>c[e]=()=>o[e]);return c.default=()=>o,r.d(a,c),a}})(),r.d=(e,t)=>{if(Array.isArray(t))for(var o=0;oPromise.all(Object.keys(r.f).reduce((t,o)=>(r.f[o](e,t),t),[])),r.u=e=>"assets/js/"+({3:"7cf461fa",48:"a94703ab",98:"a7bd4aaa",305:"dc729c33",401:"17896441",502:"ed1a65a0",515:"a1985327",566:"3c097e0f",571:"c0b876b6",609:"fc54c9f2",647:"5e95c892",671:"dbd2121f",703:"202fc7b2",742:"aba21aa0"}[e]||e)+"."+{3:"1cb4b3cb",48:"c13f04ba",98:"2eacc047",237:"7f505004",305:"243c0d96",401:"02cdecdc",502:"813e3890",515:"de92e49f",566:"29dc8546",571:"fd1bf75e",609:"1d19854c",647:"00e4eac4",671:"11849500",703:"fc87f7c1",742:"a2338f79"}[e]+".js",r.miniCssF=e=>{},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{const e={},t="coro-solto-docs:";r.l=(o,n,a,c)=>{if(e[o])return void e[o].push(n);let s,f;if(void 0!==a){const e=document.getElementsByTagName("script");for(var i=0;i{s.onerror=s.onload=null,clearTimeout(u);const n=e[o];if(delete e[o],s.parentNode?.removeChild(s),n?.forEach(e=>e(r)),t)return t(r)},u=setTimeout(l.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=l.bind(null,s.onerror),s.onload=l.bind(null,s.onload),f&&document.head.appendChild(s)}})(),r.r=e=>{Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.dn=e=>{var t=Object.getOwnPropertyDescriptor(e,"name");(!t||!t.writable&&t.configurable)&&Object.defineProperty(e,"name",{value:"default",configurable:!0})},r.cjs=e=>{const t={exports:{}};return e.call(t.exports,t,t.exports),t.exports},r.p="/docs/en/",r.gca=function(e){return e={17896441:"401","7cf461fa":"3",a94703ab:"48",a7bd4aaa:"98",dc729c33:"305",ed1a65a0:"502",a1985327:"515","3c097e0f":"566",c0b876b6:"571",fc54c9f2:"609","5e95c892":"647",dbd2121f:"671","202fc7b2":"703",aba21aa0:"742"}[e]||e,r.p+r.u(e)},(()=>{const e={354:0,869:0};r.f.j=(t,o)=>{let n=r.o(e,t)?e[t]:void 0;if(0!==n)if(n)o.push(n[2]);else if(/^(354|869)$/.test(t))e[t]=0;else{const a=new Promise((r,o)=>n=e[t]=[r,o]);o.push(n[2]=a);const c=r.p+r.u(t),s=new Error,f=o=>{if(r.o(e,t)&&(n=e[t],0!==n&&(e[t]=void 0),n)){const e=o&&("load"===o.type?"missing":o.type),r=o&&o.target&&o.target.src;s.message="Loading chunk "+t+" failed.\n("+e+": "+r+")",s.name="ChunkLoadError",s.type=e,s.request=r,s.event=o,n[1](s)}};r.l(c,f,"chunk-"+t,t)}},r.O.j=t=>0===e[t];const t=(t,o)=>{let[n,a,c]=o;var s,f,i=0;if(n.some(t=>0!==e[t])){for(s in a)r.o(a,s)&&(r.m[s]=a[s]);if(c)var l=c(r)}for(t&&t(o);i BotBrain | CORO SOLTO — Docs do Dev - + diff --git a/public/docs/en/contributing/index.html b/public/docs/en/contributing/index.html index 5d39b6a5f..dab7a8d58 100644 --- a/public/docs/en/contributing/index.html +++ b/public/docs/en/contributing/index.html @@ -4,7 +4,7 @@ How to contribute | CORO SOLTO — Docs do Dev - + @@ -13,7 +13,7 @@

The number below is not rhetoric, and it is not hand-written: it comes from git shortlog -sn --no-merges minus the authors that are AI agents (which sign as Claude / Claude (gauntlet …)).

-

11 human author identities sign commits in this branch: ruben-cytonic, Emerson Garrido, Ruben, rubenmarcus, Ruben Marcus, William Oliveira, Juan Versolato Lopes, daeeseD, matheusgb, Maná Soares, daltonfontes. Automated identities are excluded. A Git author name is not necessarily one unique person.

+

11 human author identities sign commits in this branch: ruben-cytonic, Ruben, Emerson Garrido, rubenmarcus, Ruben Marcus, William Oliveira, Juan Versolato Lopes, daeeseD, matheusgb, Maná Soares, daltonfontes. Automated identities are excluded. A Git author name is not necessarily one unique person.

Block generated by node tools/gen-docs.mjs. Source: git shortlog -sn --no-merges (descontando autores que são agentes)

diff --git a/public/docs/en/index.html b/public/docs/en/index.html index 7fc3b6fe4..80c16d721 100644 --- a/public/docs/en/index.html +++ b/public/docs/en/index.html @@ -4,7 +4,7 @@ What it is, and how to run it | CORO SOLTO — Docs do Dev - + @@ -22,7 +22,7 @@ this page was aging at the very first commit — see what is generated, and what is not.

-
WhatHow muchWhere to check
Game code31,744 lines in 44 filesgit ls-files public/js/*.js | xargs wc -l
game.js6,838 lineswc -l public/js/game.js
main.js2,646 lineswc -l public/js/main.js
Weapons with GLB26git ls-files 'public/models/weapons/*.glb' | wc -l
Character GLBs45git ls-files 'public/models/characters/*.glb' | wc -l
Props in GLB108git ls-files 'public/models/props/*.glb' | wc -l
Versioned animation clips573git ls-files public/models/anims | wc -l
Playable characters44, in 5 factionsCHARACTERS array in characters.js
Maps in the registry12MAPS object in maps.js
Visual harnesses in HTML15git ls-files 'public/*.html' | wc -l
Harness scripts192git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' | wc -l
Pipeline scripts54git ls-files 'tools/*.mjs' | wc -l
Written entry tasks26git ls-files 'docs/issues/[0-9]*.md' | wc -l
Version2.0.0-alpha.169public/js/version.js and package.json (match)
+
WhatHow muchWhere to check
Game code32,001 lines in 44 filesgit ls-files public/js/*.js | xargs wc -l
game.js6,910 lineswc -l public/js/game.js
main.js2,698 lineswc -l public/js/main.js
Weapons with GLB26git ls-files 'public/models/weapons/*.glb' | wc -l
Character GLBs45git ls-files 'public/models/characters/*.glb' | wc -l
Props in GLB108git ls-files 'public/models/props/*.glb' | wc -l
Versioned animation clips573git ls-files public/models/anims | wc -l
Playable characters44, in 5 factionsCHARACTERS array in characters.js
Maps in the registry12MAPS object in maps.js
Visual harnesses in HTML15git ls-files 'public/*.html' | wc -l
Harness scripts200git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' | wc -l
Pipeline scripts54git ls-files 'tools/*.mjs' | wc -l
Written entry tasks26git ls-files 'docs/issues/[0-9]*.md' | wc -l
Version2.0.0-alpha.179public/js/version.js and package.json (match)

Block generated by node tools/gen-docs.mjs. Source: the command in the right column of each row

@@ -112,8 +112,8 @@

Comman
npm run dev            # site + game (Astro, :4321) — the / route IS the game
npm run build # dist/client + dist/server
npm run eval:vm # viewmodel framing — RUN BEFORE the invariants
npm run eval:invariants # the invariants — pure node, 10-12 min
npm run eval:bots # botsim 60 s per map, fixed seeds
npm run eval:mat # material/light/fog/texture on the maps
npm run docs # regenerates the numeric blocks of this documentation
node tools/eval/serve.mjs 8123 # static server without Astro

And the two gates, with the exact list of what each one runs — straight from package.json:

-
npm run check:fast   # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:preload eval:docsautoria
-

package.json has 117 scripts; the reason behind each one lives in SCRIPTS.md.

+
npm run check:fast   # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam
+

package.json has 124 scripts; the reason behind each one lives in SCRIPTS.md.

Block generated by node tools/gen-docs.mjs. Source: node -p "Object.keys(require('./package.json').scripts)"

diff --git a/public/docs/en/quality-gates/index.html b/public/docs/en/quality-gates/index.html index 4be7fa7d4..09a7d2435 100644 --- a/public/docs/en/quality-gates/index.html +++ b/public/docs/en/quality-gates/index.html @@ -4,7 +4,7 @@ The gate: invariants, provenance and mutation | CORO SOLTO — Docs do Dev - + @@ -16,7 +16,7 @@
  • tools/eval/invariants.mjs: 2,275 lines, 65 declared invariant identifiers, with 28 declared skip() paths.
  • -
  • The harness contains 192 scripts in tools/eval/, plus 54 pipeline scripts in tools/.
  • +
  • The harness contains 200 scripts in tools/eval/, plus 54 pipeline scripts in tools/.
  • The number of critical checks in one run depends on the inputs present on that machine; dated results belong in KNOWN-BUGS.md.
grep -o "put('[A-Z0-9_]*'" tools/eval/invariants.mjs | sort -u | wc -l
grep -o "skip('[A-Z0-9_]*'" tools/eval/invariants.mjs | sort -u | wc -l
diff --git a/public/docs/en/stack/index.html b/public/docs/en/stack/index.html index 8b1c91be9..e55f989c1 100644 --- a/public/docs/en/stack/index.html +++ b/public/docs/en/stack/index.html @@ -4,7 +4,7 @@ Stack and tools | CORO SOLTO — Docs do Dev - + @@ -14,8 +14,8 @@ version, not the remembered one. The table below is generated by node tools/gen-docs.mjs from package.json, docs/package.json and the vendored Three.js itself.

-
LayerToolVersion
3D engine (WebGL)Three.js, vendoredr160
Gamevanilla ES modules, zero build44 files
SiteAstro with SSR^7.1.1
HostingVercel adapter^11.0.3
Databasemanaged Postgres (RLS; private schema)^2.110.7
Browser checksPlaywright^1.62.1
GLB pipelinegltf-transform^4.4.1
Mesh compressionmeshoptimizer^1.2.0
Imagessharp · resvg^0.35.3 · ^2.6.2
This documentationDocusaurus3.6.3
CI runtimeNode22
-

Three.js comes from public/vendor/three.module.js. Of the scripts in tools/, 109 import Playwright, 37 import gltf-transform, and 4 import meshoptimizer.

+
LayerToolVersion
3D engine (WebGL)Three.js, vendoredr160
Gamevanilla ES modules, zero build44 files
SiteAstro with SSR^7.1.1
HostingVercel adapter^11.0.6
Databasemanaged Postgres (RLS; private schema)^2.110.7
Browser checksPlaywright^1.62.1
GLB pipelinegltf-transform^4.4.1
Mesh compressionmeshoptimizer^1.2.0
Imagessharp · resvg^0.35.3 · ^2.6.2
This documentationDocusaurus3.6.3
CI runtimeNode22
+

Three.js comes from public/vendor/three.module.js. Of the scripts in tools/, 110 import Playwright, 37 import gltf-transform, and 4 import meshoptimizer.

Block generated by node tools/gen-docs.mjs. Source: dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js

diff --git a/public/docs/en/status/index.html b/public/docs/en/status/index.html index 921306590..6a8525c5b 100644 --- a/public/docs/en/status/index.html +++ b/public/docs/en/status/index.html @@ -4,7 +4,7 @@ Current state: production, data, and debt | CORO SOLTO — Docs do Dev - + diff --git a/public/docs/estado/index.html b/public/docs/estado/index.html index 05102b3fe..38192021b 100644 --- a/public/docs/estado/index.html +++ b/public/docs/estado/index.html @@ -4,7 +4,7 @@ Estado atual: produção, dados e dívidas | CORO SOLTO — Docs do Dev - + diff --git a/public/docs/index.html b/public/docs/index.html index a506c00a5..fae0a2a39 100644 --- a/public/docs/index.html +++ b/public/docs/index.html @@ -4,7 +4,7 @@ O que é, e como rodar | CORO SOLTO — Docs do Dev - + @@ -20,7 +20,7 @@ esta página envelhecia no primeiro commit — ver o que é gerado, e o que não é.

-
O queQuantoOnde confere
Código do jogo31.744 linhas em 44 arquivosgit ls-files public/js/*.js | xargs wc -l
game.js6.838 linhaswc -l public/js/game.js
main.js2.646 linhaswc -l public/js/main.js
Armas com GLB26git ls-files 'public/models/weapons/*.glb' | wc -l
GLBs de personagem45git ls-files 'public/models/characters/*.glb' | wc -l
Props em GLB108git ls-files 'public/models/props/*.glb' | wc -l
Clipes de animação versionados573git ls-files public/models/anims | wc -l
Personagens jogáveis44, em 5 facçõesarray CHARACTERS de characters.js
Mapas no registro12objeto MAPS de maps.js
Arnêses visuais em HTML15git ls-files 'public/*.html' | wc -l
Scripts do arnês192git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' | wc -l
Scripts de pipeline54git ls-files 'tools/*.mjs' | wc -l
Tarefas de entrada escritas26git ls-files 'docs/issues/[0-9]*.md' | wc -l
Versão2.0.0-alpha.169public/js/version.js e package.json (batem)
+
O queQuantoOnde confere
Código do jogo32.001 linhas em 44 arquivosgit ls-files public/js/*.js | xargs wc -l
game.js6.910 linhaswc -l public/js/game.js
main.js2.698 linhaswc -l public/js/main.js
Armas com GLB26git ls-files 'public/models/weapons/*.glb' | wc -l
GLBs de personagem45git ls-files 'public/models/characters/*.glb' | wc -l
Props em GLB108git ls-files 'public/models/props/*.glb' | wc -l
Clipes de animação versionados573git ls-files public/models/anims | wc -l
Personagens jogáveis44, em 5 facçõesarray CHARACTERS de characters.js
Mapas no registro12objeto MAPS de maps.js
Arnêses visuais em HTML15git ls-files 'public/*.html' | wc -l
Scripts do arnês200git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' | wc -l
Scripts de pipeline54git ls-files 'tools/*.mjs' | wc -l
Tarefas de entrada escritas26git ls-files 'docs/issues/[0-9]*.md' | wc -l
Versão2.0.0-alpha.179public/js/version.js e package.json (batem)

Bloco gerado por node tools/gen-docs.mjs. Fonte: o comando da coluna direita de cada linha

@@ -110,8 +110,8 @@

npm run dev            # site + jogo (Astro, :4321) — a rota / JÁ É o jogo
npm run build # dist/client + dist/server
npm run eval:vm # enquadramento do viewmodel — RODE ANTES das invariantes
npm run eval:invariants # as invariantes — node puro, 10-12 min
npm run eval:bots # botsim 60 s por mapa, sementes fixas
npm run eval:mat # material/luz/fog/textura nos mapas
npm run docs # regenera os blocos numéricos desta documentação
node tools/eval/serve.mjs 8123 # servidor estático sem Astro

E os dois quality gates, com a lista exata do que cada um roda — direto do package.json:

-
npm run check:fast   # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:preload eval:docsautoria
-

package.json tem 117 scripts; o motivo de cada um mora em SCRIPTS.md (migrado das chaves //nome em 18/08/2026) — é onde está o porquê.

+
npm run check:fast   # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam
+

package.json tem 124 scripts; o motivo de cada um mora em SCRIPTS.md (migrado das chaves //nome em 18/08/2026) — é onde está o porquê.

Bloco gerado por node tools/gen-docs.mjs. Fonte: node -p "Object.keys(require('./package.json').scripts)"

diff --git a/public/docs/instrumentacao-ai/index.html b/public/docs/instrumentacao-ai/index.html index c4b1d4846..e1e92b5c8 100644 --- a/public/docs/instrumentacao-ai/index.html +++ b/public/docs/instrumentacao-ai/index.html @@ -4,7 +4,7 @@ Instrumentação de IA: como o trabalho é feito | CORO SOLTO — Docs do Dev - + diff --git a/public/docs/quality-gates/index.html b/public/docs/quality-gates/index.html index 370297b64..193cf2675 100644 --- a/public/docs/quality-gates/index.html +++ b/public/docs/quality-gates/index.html @@ -4,7 +4,7 @@ O quality gate: invariantes, procedência e mutação | CORO SOLTO — Docs do Dev - + @@ -15,7 +15,7 @@
  • tools/eval/invariants.mjs: 2.275 linhas, 65 identificadores de invariante declarados (put()), dos quais 28 têm caminho de skip() declarado.
  • -
  • O arnês inteiro são 192 scripts em tools/eval/ (.mjs + .py), mais 54 scripts de pipeline em tools/.
  • +
  • O arnês inteiro são 200 scripts em tools/eval/ (.mjs + .py), mais 54 scripts de pipeline em tools/.
  • Quantas invariantes rodam como críticas numa execução não é derivável do fonte: depende de qual insumo existe na máquina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse número só sai rodando o quality gate — e o lugar dele é o cabeçalho do KNOWN-BUGS.md, atualizado com saída real.

Reproduza:

diff --git a/public/docs/stack/index.html b/public/docs/stack/index.html index 6c576e1e2..4d7b3c616 100644 --- a/public/docs/stack/index.html +++ b/public/docs/stack/index.html @@ -4,7 +4,7 @@ Stack e ferramentas | CORO SOLTO — Docs do Dev - + @@ -13,8 +13,8 @@ declarada, não com a lembrada. A tabela abaixo é gerada por node tools/gen-docs.mjs a partir do package.json, do docs/package.json e do próprio Three.js vendorizado.

-
CamadaFerramentaVersão
Motor 3D (WebGL)Three.js, vendorizador160
JogoES modules vanilla, zero build44 arquivos
SiteAstro com SSR^7.1.1
Hospedagemadapter Vercel^11.0.3
BancoPostgres gerenciado (RLS; schema privado, fora do repo)^2.110.7
Browser nas réguasPlaywright^1.62.1
Pipeline de GLBgltf-transform^4.4.1
Compressão de malhameshoptimizer^1.2.0
Imagem (build e API)sharp · resvg^0.35.3 · ^2.6.2
Esta documentaçãoDocusaurus3.6.3
Runtime de CINode22
-

Three.js sai de public/vendor/three.module.js (sem CDN, sem npm no runtime). Astro e Vercel de package.json + astro.config.mjs + vercel.json. Dos scripts de tools/, 109 importam Playwright, 37 importam gltf-transform e 4 importam meshoptimizer.

+
CamadaFerramentaVersão
Motor 3D (WebGL)Three.js, vendorizador160
JogoES modules vanilla, zero build44 arquivos
SiteAstro com SSR^7.1.1
Hospedagemadapter Vercel^11.0.6
BancoPostgres gerenciado (RLS; schema privado, fora do repo)^2.110.7
Browser nas réguasPlaywright^1.62.1
Pipeline de GLBgltf-transform^4.4.1
Compressão de malhameshoptimizer^1.2.0
Imagem (build e API)sharp · resvg^0.35.3 · ^2.6.2
Esta documentaçãoDocusaurus3.6.3
Runtime de CINode22
+

Three.js sai de public/vendor/three.module.js (sem CDN, sem npm no runtime). Astro e Vercel de package.json + astro.config.mjs + vercel.json. Dos scripts de tools/, 110 importam Playwright, 37 importam gltf-transform e 4 importam meshoptimizer.

Bloco gerado por node tools/gen-docs.mjs. Fonte: dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js

diff --git a/public/img/map-previews/parque_treta.jpg b/public/img/map-previews/parque_treta.jpg index 0ccf5a236..5ab549806 100644 Binary files a/public/img/map-previews/parque_treta.jpg and b/public/img/map-previews/parque_treta.jpg differ diff --git a/public/img/map-previews/penitenciaria.jpg b/public/img/map-previews/penitenciaria.jpg index c00ed2300..994d1003b 100644 Binary files a/public/img/map-previews/penitenciaria.jpg and b/public/img/map-previews/penitenciaria.jpg differ diff --git a/public/img/map-previews/velho_oeste.jpg b/public/img/map-previews/velho_oeste.jpg index ed6df90c1..fb268e776 100644 Binary files a/public/img/map-previews/velho_oeste.jpg and b/public/img/map-previews/velho_oeste.jpg differ diff --git a/public/js/game.js b/public/js/game.js index 2a070654b..c9d32beeb 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -16,17 +16,11 @@ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { frase, tr } from './i18n.js'; // EN por camada — o crash 'frase is not defined' de 06/08 foi este import faltando // cor de facção: UMA origem, importada também por brasoes.js e characters.js. O espelho // que este import substitui apagou a bandeira do jogador em 07/08 — ver paleta.js. -// A origem é o registro do elenco: `paleta.js` nasceu no mesmo conserto mas só cobre as 5 -// primeiras facções, e os hexes das 5 são idênticos nos dois módulos. -import { factionColor, factionInk, factionName, factionTag } from './factions.js'; -// ESPELHO não é cor de facção: é o roxo do inimigo da MESMA facção do jogador. Continua -// vindo de paleta.js, que é onde a main o tirou dos literais espalhados por aqui. -import { ESPELHO } from './paleta.js'; +import { tons, ESPELHO } from './paleta.js'; import { PlayerRecorder } from './botbrain/recorder.js'; // BOTBRAIN: grava (estado→ação) do jogador (só quando recordTraining) import { buildState } from './botbrain/features.js'; // BOTBRAIN: monta o vetor de estado do bot p/ a rede import { sense } from './botbrain/sense.js'; // BOTBRAIN: percepção (jogo→features) import { BotBrain } from './botbrain/brain.js'; // BOTBRAIN: inferência (rede treinada rodando no bot) -import { createSoundscape } from './soundscape.js'; // vida 1: áudio ambiente por mapa (world.sound) import { WEAPONS } from './data/weapons.js'; // Reexporta pra não quebrar quem já consumia a tabela daqui: server/room.js (servidor @@ -36,8 +30,17 @@ export { WEAPONS }; ?pace=0 -> round volta a ser SÓ tempo (sem alvo de abates, sem match point) ?move=0 -> movimento volta ao modelo antigo (4.7 base, sprint 6.6, sem counter-strafe) ?killcam=0 -> sem painel/câmera de morte + ?replaycam=0 -> sem replay cam ao matar (câmera orbital na vítima) Motivo: as três mudam COMPORTAMENTO sentido pelo jogador; o dono precisa do A/B. */ const QS = new URLSearchParams(location.search); +const REPLAY_CAM = QS.get('replaycam') !== '0'; +/* Replay cam (kill-switch ?replaycam=0): duração total em s, escala de dt do hit-stop e a + janela dele em tempo real, e o raio/altura da órbita em torno da vítima. */ +const REPLAY_DUR = 1.2; +const REPLAY_SLOWMO = 0.18; +const REPLAY_SLOWMO_DUR = 0.2; +const REPLAY_ORBIT_R = 3.2; +const REPLAY_ORBIT_H = 1.8; // ?vmlab=1 usa o viewmodel afinado; sem a flag mantém o calibrado. const VMLAB = QS.get('vmlab') === '1'; /* KILL-SWITCH DA RODADA DE MATERIAL: ?vmmat=legacy devolve, de uma vez, o clamp @@ -252,21 +255,6 @@ const MOVE_MUL = { }; const WALK_MUL = 0.52; // Shift: 52% da velocidade, sem som de passo const MOVE2 = QS.get('move') !== '0'; -/* DEGRAU do corpo. Estava declarado DENTRO do `_updatePlayer` e o mantle precisa do mesmo - número — duplicar seria o instrumento discordando de si (é 0,55 que separa "degrau" de - "beirada", e os dois lados dessa fronteira têm que ler a MESMA constante). */ -const STEP_H = 0.55; -/* MANTLING — subir em beirada com as mãos. Procedência de cada número e o caso do canal - do Córrego: cabeçalho de tools/eval/mantle-check.mjs. Kill-switch: `?mantle=0`. */ -const MANTLE = QS.get('mantle') !== '0'; -const MANTLE_H = 1.95; -const MANTLE_D0 = 0.58; -const MANTLE_ALC = 1.50; -const MANTLE_APOIO = 0.50; -/* MANTLE_GRID = 0,75 m — passo da grade de alcance (ver `_mantleAlcance`). Menor que o - DIÂMETRO do corpo (0,76 m): a inundação não escorre por uma fresta por onde o corpo não - passaria. Maior que isso e ela vazaria; muito menor e custa tempo à toa. */ -const MANTLE_GRID = 0.75; /* KILL-SWITCH DO ARMÁRIO DO SPAWN (P3, 01/08). `?rack=old` traz de volta o layout cego (2 fileiras fixas a 2,0/3,25 m atrás do spawn, x absoluto) E a seleção antiga (só a arma mais próxima em 1,9 m) — os dois juntos são o bug que o dono reportou às 20:38, e ficam @@ -301,6 +289,7 @@ const RACK_RETA = QS.get('rackreta') === '1'; A simetria é parte do desenho: vale pra jogador E bots — meia regeneração faria o bot virar esponja. Régua: invariante REGEN de `tools/eval/regen-check.mjs`. */ const REGEN = QS.get('regen') === '1', REGEN_DELAY = 6, REGEN_RATE = 22; +const TEAM_LABEL = { E: 'TIME E', B: 'TIME B' }; const RADIO = { z: { title: 'COMANDOS', items: ['Bora, bora, bora!', 'Cobre eu!', 'Recua, recua!'] }, x: { title: 'RESPOSTAS', items: ['Recebido!', 'Negativo!', 'Bonito tiro!'] }, @@ -313,12 +302,6 @@ const MK_LABELS = { doublekill: 'DOUBLE KILL', triplekill: 'TRIPLE KILL', multik caixa, sem padrão, sem falloff). Existe porque isto muda o COMPORTAMENTO de mira das 26 armas de uma vez — se algo ficar ruim em produção o dono tem o A/B na querystring. */ const GUNFEEL = new URLSearchParams(location.search).get('gunfeel') !== '0'; -/* PUNCH (dono, 17/08: "feel fraco, sem punch"): concussão POR DISPARO que não mexe em - balística nem no recuo medido (vm-kick-sim) — pulso de camera.zoom (~1,4%, decai em - ~80 ms), flash/luz de boca ~30% maiores e duck do mix no tiro synth. ?punch=0 desliga. */ -const PUNCH = new URLSearchParams(location.search).get('punch') !== '0'; -const PUNCH_ZOOM = 0.014, PUNCH_DECAY = 13; -const TRACER_STYLE = Object.freeze({ radius: .0115, travel: .044, fade: .014, segment: 1.45 }); // Kill-switch: ?blood=0 desliga o sangue (spray + mancha em parede/chão + poça sob o // cadáver). Muda o "gore" sentido pelo jogador — flag pro A/B do dono, como ?gunfeel=0. const BLOOD = new URLSearchParams(location.search).get('blood') !== '0'; @@ -585,9 +568,25 @@ export function pickMatchRoster(playerFaction, enemyFaction, teamSize, playerCha }; } +/* Pool dos bots em modo `all`: uma fonte só para o sorteio da partida (pickMatchWeapons) e + para o fallback do Game — duas listas divergiriam em silêncio e furariam o preload. */ +const BOT_WEAPON_POOL = ['awp', 'ak', 'm4', 'mp5', 'shotgun', 'deagle', 'm92', 'akm', 'md97', + 'carbine', 'm400', 'mosin', 'rem700', 'lmg', 'scar', 'g3', 'tavor', 'famas', 'uzi', 'p90', 'revolver38']; + +/* Armas da partida sorteadas ANTES do preload, pelo mesmo motivo do roster: o main.js precisa + saber o que carregar, e re-sortear no Game poria arma de caixa em bot. Régua: ARM1. */ +export function pickMatchWeapons({ mode = 'all', teamSize = 8 } = {}) { + const um = () => (mode === 'awp' ? 'awp' : mode === 'knife' ? 'knife' + : mode === 'pistols' ? (Math.random() < 0.5 ? 'pistol' : 'deagle') + : BOT_WEAPON_POOL[(Math.random() * BOT_WEAPON_POOL.length) | 0]); + return Array.from({ length: Math.max(1, teamSize) * 2 }, um); +} + export class Game { - constructor({ renderer, textures, sfx, settings, playerCharId, playerTeam, playerFaction, enemyFaction, nickname, mapId, ctf, roundsMax, testMode = false, mobile = false, matchRoster = null, onQuit, onMatchEnd, onTrainingFrames, recordTraining = false }) { + constructor({ renderer, textures, sfx, settings, playerCharId, playerTeam, playerFaction, enemyFaction, nickname, mapId, ctf, roundsMax, testMode = false, mobile = false, matchRoster = null, matchWeapons = null, onQuit, onMatchEnd, onTrainingFrames, recordTraining = false }) { this._ctfOpt = ctf; + this._matchWeapons = matchWeapons; + this._armaN = 0; this.renderer = renderer; this.sfx = sfx; this.settings = settings; @@ -736,7 +735,9 @@ export class Game { // tem elenco — ver pickMatchRoster) vem sorteado do main.js ou é sorteado aqui (arnês/testes). const { allyDefs, enemyDefs } = matchRoster || pickMatchRoster(this.playerFaction, this.enemyFaction, teamSize, playerCharId); const mkBot = (def, team, i) => { - const wpn = this._botWeapon(); + // arma sorteada no main.js (que preloadou por ela); sem lista, sorteia aqui. Contador + // próprio: `i` reinicia por LADO e daria a mesma arma aos dois times. + const wpn = this._matchWeapons?.[this._armaN++] || this._botWeapon(); const c = buildCharacterModel(def, { weaponId: wpn }) || buildCharacter(def); c.group.traverse(o => { o.userData.botOwner = null; }); const bot = { @@ -1038,7 +1039,7 @@ export class Game { // tracer mesh pool (shared unit geometry + material; reused, never disposed per shot). // Estilo Claude-of-Duty (fx/tracers.js): rastro FINO branco-quente que VIAJA da boca ao // alvo e some em ~50-60ms — projétil passando, não "raio laser" amarelo persistente. - this._tracerGeo = new THREE.CylinderGeometry(TRACER_STYLE.radius, TRACER_STYLE.radius, 1, 5, 1, true); + this._tracerGeo = new THREE.CylinderGeometry(0.0035, 0.0035, 1, 5, 1, true); // fino (era 0.0065 — "lightsaber branca" em cena clara) this._tracerMat = new THREE.MeshBasicMaterial({ color: 0xfff3d6, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false }); this._tracerPool = []; // Pose de ADS (iron-sight) POR CLASSE (R7.5): delta aplicado ao vm.root conforme adsF @@ -1099,11 +1100,6 @@ export class Game { this.roundCaps = { E: 0, B: 0 }; // capturas DESTA rodada (o ctfCaps é da partida toda) this.matchKills = { E: 0, B: 0 }; // abates das rodadas JÁ FECHADAS (desempate do _endMatch) this.timeLeft = ROUND_TIME; - /* MODO ARENA: mata-mata onde quem morre troca de time. O jogo corre sem rounds, - respawn contínuo, e a partida acaba quando um time fica com ≤ 1 jogador. - ?arena=1 ativa. */ - this.arena = QS.get('arena') === '1'; - this.arenaSwitched = 0; // contador de trocas (pro HUD) /* game.js:944 — RELÓGIO DE PARTIDA DO CAPTURA (não é relógio de round). Só o modo CTF usa; no modo de abate fica Infinity e nada o lê. Ele NÃO reinicia a cada rodada (é o que o diferencia do `timeLeft`) e só aparece no HUD nos últimos @@ -1234,7 +1230,7 @@ export class Game { const pal = (pdef && pdef.pal) || { skin: 0xd9a066, shirt: 0x3a4a5a }; // LUVA POR TIME no fallback procedural também (mãos genéricas por time — pedido do dono): // P vermelho, B verde, U roxo; blend 55% (igual ao fparms) pra não virar luva plástica. - const GLOVE = { E: 0xd83232, B: 0x28c858, U: 0x8a3ffc, C: 0xf0f0f0, F: 0xffc233, M: 0x9d4edd }; + const GLOVE = { E: 0xd83232, B: 0x28c858, U: 0x8a3ffc }; const skinMat = dark(pal.skin); if (GLOVE[this.playerFaction]) skinMat.color.lerp(new THREE.Color(GLOVE[this.playerFaction]), 0.85); const sleeveMat = dark(pal.shirt); @@ -2111,138 +2107,15 @@ export class Game { const cat = RADIO[this.radioOpen]; const item = cat.items[n - 1]; if (!item) return; - // O primeiro comando é também um ping de ROTA. Ele desenha só o caminho já - // percorrido — nunca posição inimiga — e dá ao Motoca um uso concreto para a carga. - const routeSecs = this.radioOpen === 'z' && n === 1 ? this._routePing() : 0; - this.sfx.characterVoice(this.playerCharId, 'radio', { fallbackFaction: this._voiceKey(this.playerTeam), interrupt: true }); + this.sfx.radioVoice(this._voiceKey(this.playerTeam)); const log = document.createElement('div'); log.className = 'radio-line'; - log.textContent = `${this.player.name} (${tr('RÁDIO')}): ${item}` + - (routeSecs ? ` · ROTA ${routeSecs.toFixed(0)}s` : ''); + log.textContent = `${this.player.name} (${tr('RÁDIO')}): ${item}`; this.el.radioLog.appendChild(log); setTimeout(() => log.remove(), 4200); while (this.el.radioLog.children.length > 3) this.el.radioLog.firstChild.remove(); } - /* ================= mecânicas das vertical slices ================= - Helpers pequenos e testáveis: a ficha vive no motor sem espalhar condicionais de - personagem por dano, movimento, rádio e CTF. Nenhum deles altera dano ou velocidade. */ - _abilityNotice(text, life = 3200) { - if (!this.el?.radioLog) return; - const log = document.createElement('div'); - log.className = 'radio-line ability-line'; - log.textContent = text; - this.el.radioLog.appendChild(log); - setTimeout(() => log.remove(), life); - while (this.el.radioLog.children.length > 3) this.el.radioLog.firstChild.remove(); - } - - _resetSliceAbilities() { - const p = this.player; - p._motocaRun = 0; p._motocaPingReady = false; - p._pieceReady = this.playerCharId === 'doidinho-bairro'; p._pieceObjectiveId = null; - p._routeTrail = [{ x: p.pos.x, z: p.pos.z }]; - p._stackBearing = null; p._stackUntil = 0; - this._stackTraceEvent = null; - } - - _stackTrace(attacker) { - const p = this.player; - if (this.playerCharId !== 'programador-virado' || !p.alive || p.hp <= 0 || !attacker?.alive || !attacker.pos) return false; - const from = p.pos.clone(); from.y += 1.35; - const to = attacker.pos.clone(); to.y += 1.35; - // Agressor atrás de parede/fumaça não vira informação de time. Granadas podem causar - // dano sem LOS, por isso esta guarda não é redundante com o hitscan. - if (!this._losClear(from, to)) return false; - const STEP = Math.PI / 4; - const raw = Math.atan2(attacker.pos.x - p.pos.x, attacker.pos.z - p.pos.z); - const bearing = Math.round(raw / STEP) * STEP; // oitante, nunca coordenada - const until = this.time + 1.2; - let allies = 0; - for (const b of this.bots) { - if (!b.alive || b.team !== p.team || b.pos.distanceTo(p.pos) > 24) continue; - b._stackBearing = bearing; b._stackUntil = until; - // Utilidade real sem telepatia: amplia por 1,2 s a sonda normal de percepção; o bot - // ainda precisa confirmar LOS antes de adquirir/atirar e não recebe o alvo. - b.alertUntil = Math.max(b.alertUntil || 0, until); - allies++; - } - const names = ['N', 'NE', 'L', 'SE', 'S', 'SO', 'O', 'NO']; - const oct = ((Math.round(bearing / STEP) % 8) + 8) % 8; - this._stackTraceEvent = { bearing, until, allies }; - if (allies) this._abilityNotice(`STACK TRACE · ameaça a ${names[oct]} · ${allies} aliado${allies > 1 ? 's' : ''}`, 1200); - return allies > 0; - } - - _updateMotocaCharge(dt, running) { - const p = this.player; - if (this.playerCharId !== 'motoca-cachorro-loko' || p._motocaPingReady) return; - p._motocaRun = running ? (p._motocaRun || 0) + dt : 0; - if (p._motocaRun + 1e-6 >= 3) { - p._motocaRun = 3; p._motocaPingReady = true; - this._abilityNotice('ROTA CONFIRMADA · próximo ping +1s'); - } - } - - _recordRoutePoint(moving) { - const p = this.player; - p._routeTrail = p._routeTrail || []; - if (!moving) return; - const last = p._routeTrail[p._routeTrail.length - 1]; - if (!last || Math.hypot(p.pos.x - last.x, p.pos.z - last.z) >= 0.65) { - p._routeTrail.push({ x: p.pos.x, z: p.pos.z }); - if (p._routeTrail.length > 14) p._routeTrail.shift(); - } - } - - _routePing() { - const p = this.player; - const bonus = this.playerCharId === 'motoca-cachorro-loko' && p._motocaPingReady ? 1 : 0; - const duration = 2 + bonus; - if (bonus) { p._motocaPingReady = false; p._motocaRun = 0; } - const pts = (p._routeTrail?.length ? p._routeTrail : [{ x: p.pos.x, z: p.pos.z }]).slice(-10); - const group = new THREE.Group(); - const geo = new THREE.RingGeometry(0.12, 0.21, 12); - const mat = new THREE.MeshBasicMaterial({ - color: this._teamColor(p.team), transparent: true, opacity: 0.82, - depthWrite: false, side: THREE.DoubleSide, - }); - for (const q of pts) { - const ring = new THREE.Mesh(geo, mat); - const y = (this.world.groundHeightAt ? this.world.groundHeightAt(q.x, q.z) : 0) + 0.055; - ring.position.set(q.x, y, q.z); ring.rotation.x = -Math.PI / 2; group.add(ring); - } - group.userData.until = this.time + duration; group.userData.life = duration; - this.scene.add(group); - this._routePings = this._routePings || []; this._routePings.push(group); - return duration; - } - - _tickRoutePings() { - for (let i = (this._routePings || []).length - 1; i >= 0; i--) { - const g = this._routePings[i], left = g.userData.until - this.time; - const opacity = Math.max(0, Math.min(0.82, left / Math.max(0.01, g.userData.life))); - for (const m of g.children) if (m.material) m.material.opacity = opacity; - if (left > 0) continue; - this.scene.remove(g); - const first = g.children[0]; first?.geometry?.dispose?.(); first?.material?.dispose?.(); - this._routePings.splice(i, 1); - } - } - - _objectiveInteractionMultiplier(pt, solo) { - const p = this.player; - const inside = p.alive && solo === p.team && - (p.pos.x - pt.x) ** 2 + (p.pos.z - pt.z) ** 2 <= pt.r * pt.r; - if (p._pieceObjectiveId === pt.id && !inside) p._pieceObjectiveId = null; - if (this.playerCharId !== 'doidinho-bairro' || !inside) return 1; - if (p._pieceObjectiveId === pt.id) return 1.25; - if (!p._pieceReady) return 1; - p._pieceReady = false; p._pieceObjectiveId = pt.id; - this._abilityNotice('TEM UMA PEÇA PRA ISSO · objetivo 20% mais rápido'); - return 1.25; // tempo ×0,8 exige taxa ×(1/0,8), não ×1,20 - } - /* ================= flow ================= */ start() { this.el.hud.classList.remove('hidden'); @@ -2260,9 +2133,6 @@ export class Game { this.roundKills = { E: 0, B: 0 }; this.roundCaps = { E: 0, B: 0 }; this.timeLeft = ROUND_TIME; - /* MODO ARENA: sem timer de round — a partida só acaba quando um time fica - com ≤ 1 jogador (_checkArenaWin). */ - if (this.arena) this.timeLeft = Infinity; this._matchPoint = false; // banner de MATCH POINT dispara uma vez por round this._resultado = null; // o título do placar é da RODADA que acabou, não da que começa // game.js:1868 — pedido de fim de rodada do CAPTURA é POR RODADA: se a rodada acabou @@ -2310,10 +2180,9 @@ export class Game { ent.hp = 100; ent.alive = true; ent.respawnAt = 0; ent.protUntil = 0; return s; }; - const playerSpawn = place(this.player, this.playerTeam, 0); - this.player.yaw = this._spawnYaw(playerSpawn, this.playerTeam, false); + place(this.player, this.playerTeam, 0); + this.player.yaw = this.playerTeam === 'E' ? Math.PI : 0; this.player.pitch = 0; this.player.vel.set(0, 0, 0); this.player.crouchF = 0; - this._resetSliceAbilities(); this.player.ammo.awp = { mag: WEAPONS.awp.mag, res: WEAPONS.awp.reserve }; this.player.ammo.pistol = { mag: WEAPONS.pistol.mag, res: WEAPONS.pistol.reserve }; this.player.smokes = 5; this.player.frags = 1; this._updateSmokeHud(); // 5 fumaças + 1 frag por round @@ -2495,8 +2364,8 @@ export class Game { this.el.weaponName.textContent = WEAPONS[this.player.weapon].name; const slots = { E: 1, B: 0 }; for (const b of this.bots) { - const botSpawn = place(b, b.team, slots[b.team]++); - b.yaw = this._spawnYaw(botSpawn, b.team, true); // o mesh aponta para +Z + place(b, b.team, slots[b.team]++); + b.yaw = b.team === 'E' ? 0 : Math.PI; // mesh forward is +Z b.target = null; b.path = null; b.repathAt = 0; b.mesh.group.rotation.set(0, b.yaw, 0); b.mesh.group.position.copy(b.pos); @@ -2731,9 +2600,6 @@ export class Game { if (this.state !== 'live' && this.state !== 'countdown') v = false; const entrou = v && !this.paused; this.paused = v; - this.soundscape?.setPaused(v); - // reset de input na pausa: a main acrescentou o toque (controles mobile) e o botão - // do mouse; a ambiência é desta branch. Os dois lados valem. if (v) { this.keys = {}; this.touchMove.x = 0; this.touchMove.z = 0; this.mouseDown0 = false; } this.el.pause.classList.toggle('hidden', !v); // pausado, o overlay de toque some — senão a camada de OLHAR (tela cheia) tapava o menu @@ -2824,7 +2690,7 @@ export class Game { swapBot.target = null; swapBot.path = null; swapBot.hp = 100; swapBot.alive = true; const s = this.world.spawns[oldTeam][(Math.random() * 4) | 0]; swapBot.pos.set(s.x, this._spawnY(s.x, s.z), s.z); - swapBot.yaw = this._spawnYaw(s, oldTeam, true); + swapBot.yaw = oldTeam === 'E' ? 0 : Math.PI; swapBot.mesh.group.rotation.set(0, swapBot.yaw, 0); swapBot.mesh.group.position.copy(swapBot.pos); swapBot.mesh.group.visible = true; @@ -2832,7 +2698,7 @@ export class Game { // respawn do jogador no lado novo const s = this.world.spawns[newTeam][(Math.random() * 4) | 0]; p.pos.set(s.x, this._spawnY(s.x, s.z), s.z); p.vel.set(0, 0, 0); - p.yaw = this._spawnYaw(s, newTeam, false); p.pitch = 0; p.hp = 100; + p.yaw = newTeam === 'E' ? Math.PI : 0; p.pitch = 0; p.hp = 100; this._scope(false, true); this._banner(frase('agoraVoceE', this._teamName(newTeam)), 'trocou de lado na treta — sem penalty, só julgamento'); this.sfx.uiClick(); @@ -3117,9 +2983,9 @@ export class Game { : (p.weapon === 'awp' ? (p.scoped ? w.spreadScope : w.spreadHip) : w.spreadHip)) * crouchMul * moveMul; const from = this.camera.getWorldPosition(new THREE.Vector3()); const pellets = w.pellets || 1; - // Pedido do dono (17/08), "todos os tiros traçados": o rastro é segmento curto - // viajante (~50 ms), não laser contínuo — por isso o antigo 1-em-3 saiu. - const wantTracer = true; + // tracer só em PARTE dos tiros (CS): 1 em 3 na rajada; sniper/shotgun sempre (o tiro é o + // evento). Antes TODO tiro deixava rastro — vira "chuva de laser" em full-auto. + const wantTracer = !GUNFEEL || pellets > 1 || (REC_DEG[p.weapon] ?? 1) > 2.4 || ((p.sprayI || 0) % 3) === 0; for (let i = 0; i < pellets; i++) { const sp = spreadBase * (1 + this.bloom); let dir; @@ -3155,8 +3021,6 @@ export class Game { : Math.min(1.5, 0.55 + (w.recoil || 0.01) * 13); this.vm.recoil.kick(vmAmp * (1 - 0.25 * p.crouchF) * kickMul); this.vm.kickSide = Math.random() * 2 - 1; - // concussão do PUNCH: pesada em tiro único, mais leve por bala em full-auto - if (PUNCH) this._punchF = Math.min(1.4, (this._punchF || 0) + (w.auto ? 0.5 : 1) * (kickMul === 0.5 ? 0.7 : 1)); const _cls = STATIC_CLASS[p.weapon] || 'rifle'; this._flash(this._muzzleWorld(_cls), this.camera.getWorldDirection(new THREE.Vector3()), _cls); this._ejectCasing(); @@ -3219,7 +3083,6 @@ export class Game { } else { end = from.clone().add(dir.clone().multiplyScalar(120)); } - this.world.ambience?.onShot(from, end); if (byPlayer && tracer) { const muzzle = this._muzzleWorld(STATIC_CLASS[this.player.weapon] || 'rifle'); this._tracer(muzzle, end); @@ -3334,7 +3197,6 @@ export class Game { if (!ent.alive || this.state !== 'live') return; if (this.time < (ent.protUntil || 0)) return; // spawn protection: zero dano (e sem hitmarker) enquanto protegido ent.hp -= dmg; - if (ent.isPlayer && ent.hp > 0) this._stackTrace(attacker); if (ent.isPlayer) { this.el.vignette.style.opacity = 0.9; setTimeout(() => this.el.vignette.style.opacity = 0, 130); @@ -3378,12 +3240,6 @@ export class Game { ent._killT = this.time; ent.alive = false; ent.hp = 0; ent.deaths++; ent.respawnAt = this.time + RESPAWN_DELAY; - /* MODO ARENA: quem morre vai pro time do assassino. A troca é aplicada no - respawn (não aqui) pra não quebrar o killfeed nem o estado do frame atual. */ - if (this.arena && attacker && attacker.team !== ent.team) { - ent._switchTeam = attacker.team; - this.arenaSwitched++; - } // Drop tem prazo e teto porque a versão sem eles foi retirada por virar lixo de mapa; // faca não dropa (todo mundo nasce com uma). Histórico e números: tools/eval/drop-check.mjs. if (ent.weapon && ent.weapon !== 'knife' && this._pickupAllowed(ent.weapon)) { @@ -3391,9 +3247,7 @@ export class Game { } if (attacker) { attacker.kills++; this.roundKills[attacker.team]++; - // characterVoice já cai no pool da facção (fallbackFaction) sem clipe próprio; - // ele substitui o antigo sfx.voice() — chamar os dois tocaria áudio dobrado. - this.sfx.characterVoice(attacker.def?.id, 'kill', { fallbackFaction: this._voiceKey(attacker.team) }); + this.sfx.voice(this._voiceKey(attacker.team)); // killer's side celebrates (meme audio) // TELEMETRIA DE ARMA: quando o JOGADOR mata, conta a arma usada (param `weap` // já vem do _damage/_tryShoot). Bot mata não conta — não há balanço a inferir. if (attacker.isPlayer && weap) this._wperf[weap] = (this._wperf[weap] || 0) + 1; @@ -3406,6 +3260,13 @@ export class Game { mk.best = Math.max(mk.best || 0, mk.count); const kind = mk.count >= 6 ? 'godlike' : (MK_TIERS[mk.count] || (mk.life === 5 ? 'killingspree' : null)); if (kind) { this._mkBanner(MK_LABELS[kind]); this.sfx.general(kind); } + if (REPLAY_CAM && head && ent.pos) { + this._replayCam = { + t: 0, + victimPos: ent.pos.clone(), + killerYaw: attacker.yaw, + }; + } } } if (ent.isPlayer) { @@ -3426,24 +3287,6 @@ export class Game { // poça que cresce sob o cadáver (qualquer morte com posição — inclui bot×bot) if (BLOOD && ent.pos) this._bloodPoolAt(ent.pos); this._feed(attacker, ent, weap, head); - /* MODO ARENA: checa se a troca de time esvaziou um lado (≤ 1 sobrando). */ - if (this.arena) this._checkArenaWin(); - } - /* ===================== MODO ARENA: condição de vitória ===================== - Conta jogadores por time (player + bots, vivos e mortos — o morto vai - trocar no respawn). Se um time ficou com ≤ 1, o outro vence. */ - _checkArenaWin() { - const all = [this.player, ...this.bots]; - const e = all.filter(e => e.team === 'E').length; - const b = all.filter(e => e.team === 'B').length; - if (e <= 1 || b <= 1) { - const winner = e > b ? 'E' : 'B'; - this.state = 'matchEnd'; - this.el.matchEnd.classList.remove('hidden'); - this.el.matchTitle.textContent = winner === this.player.team ? 'VOCÊ SOBREVIVEU!' : 'VOCÊ FOI CONVERTIDO'; - this.el.matchSub.textContent = `Time ${this._teamTag(winner)} venceu com ${Math.max(e, b)} jogadores`; - this.el.matchStats.innerHTML = `Trocas de time: ${this.arenaSwitched} · Seus abates: ${this.player.kills}`; - } } /* ===================== INDICADOR DIRECIONAL DE DANO ===================== Dono: "matam muito fácil e o usuário não vê de onde veio o tiro". O indicador antigo era @@ -3694,14 +3537,13 @@ export class Game { t.a = (t.a || new THREE.Vector3()).copy(a); t.dir = (t.dir || new THREE.Vector3()).copy(b).sub(a).normalize(); t.dist = len; - // Segmento curto e viajante: cobre pixels suficientes para ler como bala, sem - // permanecer no quadro como um laser contínuo. - t.v = len / (GUNFEEL ? TRACER_STYLE.travel : 0.05); - t.seg = Math.min(len, GUNFEEL ? TRACER_STYLE.segment : 2.0); + // GUNFEEL: mais curto e mais rápido (CS). Era 2.0 m em 50 ms; agora 1.2 m em 32 ms — a + // bala lê como bala, não como traço luminoso pendurado no ar. + t.v = len / (GUNFEEL ? 0.032 : 0.05); + t.seg = Math.min(len, GUNFEEL ? 1.2 : 2.0); t.t = 0; - t.life = (GUNFEEL ? TRACER_STYLE.travel + TRACER_STYLE.fade : 0.062); - t.ttl = t.life; - m.material.opacity = GUNFEEL ? 0.92 : 0.9; + t.ttl = (GUNFEEL ? 0.032 : 0.05) + 0.012; // viagem + fade final — sem persistência + m.material.opacity = GUNFEEL ? 0.75 : 0.9; m.position.copy(a); m.scale.set(1, 0.01, 1); m.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), t.dir); @@ -3773,8 +3615,7 @@ export class Game { if (this._vmFlashLight) this._vmFlashLight.position.copy(off); const s = 0.85 + Math.random() * 0.45; const fxf = (this._fxTune && this._fxTune.flash) ?? 1; - const pf = PUNCH ? 1.3 : 1; - m.jetS = 0.22 * s * fxf * pf; m.coreS = 0.08 * s * fxf * pf; // boca a ~0.35m da lente: menor que o do mundo + m.jetS = 0.22 * s * fxf; m.coreS = 0.08 * s * fxf; // boca a ~0.35m da lente: menor que o do mundo m.jet.scale.setScalar(m.jetS); m.core.scale.setScalar(m.coreS); m.jetMat.rotation = Math.random() * Math.PI * 2; m.jetMat.opacity = 1; m.coreMat.opacity = 1; m.grp.visible = true; m.t = 0; @@ -3786,8 +3627,7 @@ export class Game { m.grp.position.copy(pos).addScaledVector(d, 0.05); // leve viés à frente da boca const s = 0.85 + Math.random() * 0.5; // variação por tiro (0.36–0.57m no sprite) const fxf = (this._fxTune && this._fxTune.flash) ?? 1; - const pf = PUNCH ? 1.3 : 1; - m.jetS = 0.42 * s * fxf * pf; m.coreS = 0.15 * s * fxf * pf; + m.jetS = 0.42 * s * fxf; m.coreS = 0.15 * s * fxf; m.jet.scale.setScalar(m.jetS); m.core.scale.setScalar(m.coreS); m.jetMat.rotation = Math.random() * Math.PI * 2; // estrela nunca repete o ângulo m.jetMat.opacity = 1; m.coreMat.opacity = 1; m.grp.visible = true; m.t = 0; @@ -3795,7 +3635,7 @@ export class Game { } } const l = this._mzLights.pop(); - if (l) { l.position.copy(pos).addScaledVector(d, 0.12); l.intensity = 18 * ((this._fxTune && this._fxTune.light) ?? 1) * (PUNCH ? 1.35 : 1); this._mzLightActive.push({ l, t: 0, life: 0.05 }); } + if (l) { l.position.copy(pos).addScaledVector(d, 0.12); l.intensity = 18 * ((this._fxTune && this._fxTune.light) ?? 1); this._mzLightActive.push({ l, t: 0, life: 0.05 }); } // flash na CENA DO VM: pulso breve sincronizado (ilumina a arma em 1ª pessoa) if (this._vmFlash) { this._vmFlash.t = 0; if (this._vmFlashLight) this._vmFlashLight.intensity = this._vmFlash.peak * ((this._fxTune && this._fxTune.light) ?? 1); } // faíscas 3D (partículas com velocidade, encolhendo) + fumacinha. No tiro do PRÓPRIO @@ -3834,13 +3674,13 @@ export class Game { for (let i = this.tracers.length - 1; i >= 0; i--) { const t = this.tracers[i]; t.t += dt; t.ttl -= dt; - // segmento viajante: cabeça avança a t.v, cauda segue t.seg atrás; fade pela vida total + // segmento viajante: cabeça avança a t.v, cauda segue t.seg atrás; fade nos últimos 50ms const head = Math.min(t.dist, t.v * t.t); const tail = Math.max(0, head - t.seg); const vis = Math.max(0.01, head - tail); t.m.scale.y = vis; t.m.position.copy(t.a).addScaledVector(t.dir, tail + vis * 0.5); - t.m.material.opacity = 0.92 * Math.max(0, 1 - t.t / t.life); + t.m.material.opacity = 0.9 * Math.max(0, 1 - t.t / t.ttl); // fade ao longo do trajeto if (t.ttl <= 0) { this.scene.remove(t.m); this._tracerPool.push(t); this.tracers.splice(i, 1); } } // cápsulas: gravidade + quica no chão + gira; encolhe e some no fim. @@ -3884,8 +3724,7 @@ export class Game { this._vmFlashLight.intensity = f.peak * ((this._fxTune && this._fxTune.light) ?? 1) * (1 - k) * (1 - k); } else if (this._vmFlashLight.intensity !== 0) this._vmFlashLight.intensity = 0; } - this._tickRoutePings(); - } + } _ejectCasing() { if (this.player.weapon === 'knife') return; @@ -4167,10 +4006,9 @@ export class Game { // Urbanas; senão P vermelho / B verde. `dark` = tom mais escuro (pano da bandeira). _teamColor(side, dark = false) { // o que depende de partida fica aqui (quem é espelho, que facção está de cada lado); - // a COR de cada facção mora no registro do elenco e é a mesma que bandeira e rim - // consomem. O roxo do espelho sai de ESPELHO, e não mais de literal solto aqui. - if (this._mirror(side)) return dark ? ESPELHO.escura : ESPELHO.base; - return factionColor(this._factionOf(side), dark); + // a COR de cada facção mora em paleta.js e é a mesma que bandeira e rim consomem. + const p = this._mirror(side) ? ESPELHO : tons(this._factionOf(side)); + return dark ? p.escura : p.base; } /* TINTA CLARA DO TIME — só pra TEXTO sobre chip tingido (killfeed). Por que existe: o chip do killfeed é `background:${cor}2e` (a própria cor do time a 18%) com o texto @@ -4181,18 +4019,15 @@ export class Game { style.css:521-522) — a versão PÁLIDA da cor. Aqui ela ganha nome e vale pras 6 facções. Mantém a leitura "vermelho = time-e / verde = time-b" e passa a 5,9-9,1:1. */ _teamInk(side) { - if (this._mirror(side)) return ESPELHO.palida; - return factionInk(this._factionOf(side)); + return (this._mirror(side) ? ESPELHO : tons(this._factionOf(side))).palida; } // Pack de vozes/round por FACÇÃO: o lado do jogador usa 'U' (Tribos) quando a facção é Tribos // Urbanas; senão o lado (P/B). O inimigo é sempre político. Corrige "Tribos usa voz de Time E". // Facção que ocupa um LADO físico (P/B): lado do jogador = playerFaction, o outro = enemyFaction. _factionOf(side) { return side === this.playerTeam ? this.playerFaction : this.enemyFaction; } _voiceKey(side) { return this._factionOf(side); } // pack de vozes/round por facção (P/B/U) - /* Registro de facções é a origem única de nome/sigla (10 facções) — o mapa fixo da - main cobria só 5. */ - _teamName(side) { return factionName(this._factionOf(side)); } - _teamTag(side) { return factionTag(this._factionOf(side)); } + _teamName(side) { const f = this._factionOf(side); return f === 'U' ? 'TRIBOS URBANAS' : f === 'C' ? 'PALHAÇOS' : f === 'F' ? 'FUNKEIROS' : (TEAM_LABEL[f] || f); } + _teamTag(side) { const f = this._factionOf(side); return f === 'U' ? 'TRB' : f === 'C' ? 'PLH' : f === 'F' ? 'FNK' : f === 'E' ? 'TME' : 'TMB'; } /* Uma plaqueta do HUD. Chamada por QUADRO, então tudo aqui é comparação barata: o número só é escrito se mudou, e o brasão (data-f, arte no CSS) só quando a @@ -4371,12 +4206,10 @@ export class Game { pt.capTeam = solo; // time que está capturando agora (pra cor da barra no HUD) pt.contested = np > 0 && nb > 0; if (solo && solo !== pt.owner) { - let crew = Math.min(2, 1 + 0.35 * ((solo === 'E' ? np : nb) - 1)); // 2º e 3º corpo aceleram - crew *= this._objectiveInteractionMultiplier(pt, solo); + const crew = Math.min(2, 1 + 0.35 * ((solo === 'E' ? np : nb) - 1)); // 2º e 3º corpo aceleram pt.prog += (dt * crew) / (pt.owner ? CAP_STEAL : CAP_NEUTRAL); if (pt.prog >= 1) { pt.owner = solo; pt.prog = 0; - if (this.player._pieceObjectiveId === pt.id) this.player._pieceObjectiveId = null; this.sfx.captureSound && this.sfx.captureSound(this._factionOf(solo)); // captura: pool de som por facção (palhaços = pasta própria) // credita a captura: +1 pro time e +1 pra cada combatente do time DENTRO do anel this.ctfCaps[solo] = (this.ctfCaps[solo] || 0) + 1; @@ -4389,7 +4222,6 @@ export class Game { this._updateCtfHud(); } } else if (!solo) { - this._objectiveInteractionMultiplier(pt, solo); // encerra interação abandonada/contestada // CONTESTADO (os dois times no anel) CONGELA o progresso — é o momento de tensão do // modo; só decai quando o anel fica vazio ou o dono retoma sozinho. if (!pt.contested) pt.prog = Math.max(0, pt.prog - dt / (CAP_NEUTRAL * DECAY)); @@ -4797,99 +4629,6 @@ export class Game { pos.x += ex * cs + ez * sn; pos.z += -ex * sn + ez * cs; } - /* MAPA DE ALCANCE A PÉ — trava anti-exploit: o pouso do mantle só vale sobre chão que a - caminhada já alcança. Medições e custos: cabeçalho de tools/eval/mantle-check.mjs. */ - _mantleAlcance() { - if (this._mAlc) return this._mAlc; - const W = this.world, B = W.bounds, G = MANTLE_GRID; - const nx = Math.max(1, Math.floor((B.maxX - B.minX) / G)); - const nz = Math.max(1, Math.floor((B.maxZ - B.minZ) / G)); - const alt = new Float32Array(nx * nz).fill(NaN); - const p = new THREE.Vector3(); - /* PONTO REPRESENTATIVO, não o centro: num degrau o centro da célula pode cair no único - ponto ruim e a escada inteira aparecer interrompida (caso medido no fy_lajes). */ - for (let i = 0; i < nx; i++) for (let k = 0; k < nz; k++) { - const cx = B.minX + (i + 0.5) * G, cz = B.minZ + (k + 0.5) * G; - for (const [ox, oz] of [[0, 0], [0.25, 0], [-0.25, 0], [0, 0.25], [0, -0.25]]) { - const x = cx + ox, z = cz + oz, y = W.groundHeightAt(x, z, 1e3); - p.set(x, y, z); this._collide(p, 0.38); - if (Math.abs(p.x - x) < 1e-3 && Math.abs(p.z - z) < 1e-3) { alt[i * nz + k] = y; break; } - } - } - const vis = new Uint8Array(nx * nz), fila = []; - for (const lista of Object.values(W.spawns || {})) for (const s of (lista || [])) { - const i = Math.round((s.x - B.minX) / G - 0.5), k = Math.round((s.z - B.minZ) / G - 0.5); - if (i < 0 || i >= nx || k < 0 || k >= nz) continue; - const c = i * nz + k; - if (!Number.isNaN(alt[c]) && !vis[c]) { vis[c] = 1; fila.push(c); } - } - for (let h = 0; h < fila.length; h++) { - const c = fila[h], i = (c / nz) | 0, k = c % nz; - for (const [di, dk] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { - const j = i + di, l = k + dk; - if (j < 0 || j >= nx || l < 0 || l >= nz) continue; - const d = j * nz + l; - if (vis[d] || Number.isNaN(alt[d])) continue; - /* MESMA regra do corpo (subir só até STEP_H), conferida ao LONGO do trecho: - comparar só as pontas aliasa escada — no fy_lajes um degrau entre centros reprovava a laje. */ - const ax0 = B.minX + (i + 0.5) * G, az0 = B.minZ + (k + 0.5) * G; - let y = alt[c], ok = true; - for (let s = 1; s <= 3; s++) { - const sx = ax0 + di * G * (s / 3), sz = az0 + dk * G * (s / 3); - const sy = W.groundHeightAt(sx, sz, y + STEP_H); - if (sy - y > STEP_H) { ok = false; break; } - y = sy; - } - if (ok) { vis[d] = 1; fila.push(d); } - } - } - this._mAlc = { nx, nz, G, minX: B.minX, minZ: B.minZ, alt, vis }; - return this._mAlc; - } - /* O ponto (x, z) na cota `y` é chão que a caminhada já alcança? Olha a vizinhança de uma - célula porque a grade é grossa de propósito; a cota tem que BATER (dentro do degrau), - senão um beco alcançável a 1 m de distância aprovaria a laje que está 3,5 m acima. */ - _mantleAlcancavel(x, z, y) { - const M = this._mantleAlcance(); - const i0 = Math.round((x - M.minX) / M.G - 0.5), k0 = Math.round((z - M.minZ) / M.G - 0.5); - for (let i = i0 - 1; i <= i0 + 1; i++) for (let k = k0 - 1; k <= k0 + 1; k++) { - if (i < 0 || i >= M.nx || k < 0 || k >= M.nz) continue; - const c = i * M.nz + k; - if (M.vis[c] && Math.abs(M.alt[c] - y) <= STEP_H) return true; - } - return false; - } - /* ALVO DE MANTLE: devolve o ponto de pouso `{x, y, z}` ou null. ÚNICA autoridade da - decisão — tools/eval/mantle-check.mjs chama este método em vez de reimplementar a regra. */ - _mantleTarget(pos, dx, dz) { - if (!MANTLE) return null; - const W = this.world; - if (!W || typeof W.groundHeightAt !== 'function') return null; // degradação segura - const n = Math.hypot(dx, dz); - if (!(n > 1e-4)) return null; - dx /= n; dz /= n; - const chao = W.groundHeightAt(pos.x, pos.z, pos.y); - const teto = chao + MANTLE_H; - const p = new THREE.Vector3(); - for (let d = MANTLE_D0; d <= MANTLE_ALC + 1e-6; d += 0.25) { - const px = pos.x + dx * d, pz = pos.z + dz * d; - const py = W.groundHeightAt(px, pz, teto + 0.5); // Y do teto: resolve mapa multinível - p.set(px, py, pz); this._collide(p, 0.38); - if (Math.abs(p.x - px) > 1e-3 || Math.abs(p.z - pz) > 1e-3) continue; - if (py <= chao + STEP_H) return null; - if (py > teto) return null; - // APOIO: a superfície tem que continuar plana MANTLE_APOIO adiante, senão o corpo - // não cabe em cima e o mantle entrega o jogador a uma queda. - const ax = px + dx * MANTLE_APOIO, az = pz + dz * MANTLE_APOIO; - const ay = W.groundHeightAt(ax, az, py + 0.5); - if (Math.abs(ay - py) > STEP_H) return null; - p.set(ax, ay, az); this._collide(p, 0.38); - if (Math.abs(p.x - ax) > 1e-3 || Math.abs(p.z - az) > 1e-3) return null; - if (!this._mantleAlcancavel(ax, az, ay)) return null; // território novo: ver _mantleAlcance - return { x: ax, y: ay, z: az }; - } - return null; - } /* PONTO ANDÁVEL MAIS PRÓXIMO (usado pelo armário do spawn). Empurra (x,z) pra fora de qualquer colisor/limite usando a MESMA física do jogador — se o _collide não mexe no ponto, o jogador consegue ficar em pé nele; é essa a @@ -5022,24 +4761,50 @@ export class Game { c.rotation.x += (wantPitch - c.rotation.x) * Math.min(1, dt * 3.5); } } + _updateReplayCam(dt) { + const rc = this._replayCam; + if (!rc) return; + rc.t += dt; + if (rc.t >= REPLAY_DUR) { + this._replayCam = null; + const p = this.player; + const tFov = p.scoped ? this._zoomFov(p.weapon) : 70; + this.camera.fov = tFov; + this.camera.updateProjectionMatrix(); + this._fovFrom = undefined; + return; + } + const progress = rc.t / REPLAY_DUR; + const angle = rc.killerYaw + Math.PI + progress * 1.2; + const ease = 1 - (1 - progress) * (1 - progress); + const r = REPLAY_ORBIT_R * (0.6 + 0.4 * ease); + const cx = rc.victimPos.x + Math.sin(angle) * r; + const cz = rc.victimPos.z + Math.cos(angle) * r; + const cy = rc.victimPos.y + REPLAY_ORBIT_H - ease * 0.4; + this.camera.position.set(cx, cy, cz); + const lookY = rc.victimPos.y + 1.2; + const dx = rc.victimPos.x - cx, dz = rc.victimPos.z - cz; + this.camera.rotation.set( + Math.atan2(lookY - cy, Math.hypot(dx, dz)), + Math.atan2(-dx, -dz), + 0 + ); + this.camera.fov = 50; + this.camera.updateProjectionMatrix(); + if (this.vm?.root) this.vm.root.visible = false; + if (this.el.crosshair) this.el.crosshair.style.display = 'none'; + } _updatePlayer(dt) { const p = this.player; this._checkCtfAlvo(); // alvo de BANDEIRAS: única condição de vitória da rodada de CAPTURA (sem gate) if (PACE) this._checkPace(); // alvo de abates / match point — vale também com o jogador morto if (!p.alive) { - /* Morrer APAGA a escalada em curso. Sem isto ela retomaria depois do respawn e - teleportaria o corpo novo para a beirada onde o corpo velho morreu. */ - p.mantle = null; + if (this._replayCam) this._replayCam = null; const left = p.respawnAt - this.time; this.el.respawnCount.textContent = Math.max(0, left).toFixed(1); this._deathFeedback(dt); if (left <= 0) this._respawnPlayer(); - // Em mapa multinível, um piso global atravessa a laje do andar alto (caso: mirante - // do Escadão) — a queda da câmera termina acima do piso LOCAL onde o corpo morreu. - const deathFloor = (this.world.groundHeightAt - ? this.world.groundHeightAt(p.pos.x, p.pos.z, p.pos.y) - : 0) + 0.5; - this.camera.position.y = Math.max(deathFloor, this.camera.position.y - dt * 2); + this.camera.position.y = Math.max(0.5, this.camera.position.y - dt * 2); this.camera.rotation.z = Math.min(0.5, (this.camera.rotation.z || 0) + dt * 0.8); return; } @@ -5110,7 +4875,7 @@ export class Game { if (this.keys.Space && !this._spaceHeld) p.jumpBufferedUntil = this.time + 0.13; this._spaceHeld = !!this.keys.Space; if ((p.jumpBufferedUntil || 0) > this.time && this.time < (p.coyoteUntil || 0) && this._acceptInput()) { - p.vel.y = this.world.jumpImpulse ?? 5.0; p.grounded = false; p.jumpBufferedUntil = 0; p.coyoteUntil = 0; this.sfx.jump(); + p.vel.y = 5.0; p.grounded = false; p.jumpBufferedUntil = 0; p.coyoteUntil = 0; this.sfx.jump(); // apex ~0.61m (CoD) } p.vel.y -= 20.6 * dt; // gravidade exagerada do CoD — arco de pulo "snappy", não flutuante // integrate with step-limit so platform fronts block @@ -5123,7 +4888,7 @@ export class Game { // o corpo só era realinhado no snap de gravidade do frame seguinte — subir meio-fio/degrau // "engasgava"). Acima disso é parede: além de bloquear, ZERA a velocidade daquele eixo, // senão o jogador segue acelerando contra o degrau e a arma treme parada no obstáculo. - /* STEP_H mora no topo do módulo desde o mantle: é a MESMA fronteira degrau/beirada. */ + const STEP_H = 0.55; const tryAxis = (dx, dz, ax) => { const nx = p.pos.x + dx, nz = p.pos.z + dz; const g = this.world.groundHeightAt(nx, nz, p.pos.y); @@ -5140,24 +4905,6 @@ export class Game { if (!p.grounded && p.vel.y < -4) { this.sfx.land(); p.landDip = Math.min(1, -p.vel.y / 14); } // landing dip, sized by impact p.pos.y = g2; p.vel.y = 0; p.grounded = true; } else if (p.pos.y > g2 + 0.05) p.grounded = false; - /* MANTLING — fica DEPOIS da física de propósito: este bloco sobrescreve `p.pos` e - descarta o empurrão do `_collide` no meio da subida; reordenar reintroduz o BUG-32. */ - if (p.mantle) { - const m = p.mantle; - m.t += dt; - const u = Math.min(1, m.t / m.dur); - const uy = Math.min(1, u / 0.62), uxz = Math.max(0, (u - 0.35) / 0.65); - p.pos.set(m.x0 + (m.x1 - m.x0) * uxz, m.y0 + (m.y1 - m.y0) * uy, m.z0 + (m.z1 - m.z0) * uxz); - p.vel.set(0, 0, 0); p.grounded = false; - if (u >= 1) { p.pos.set(m.x1, m.y1, m.z1); p.grounded = true; p.mantle = null; } - } else if (MANTLE && (ix || iz)) { - const alvo = this._mantleTarget(p.pos, wx, wz); - if (alvo) { - const sobe = alvo.y - this.world.groundHeightAt(p.pos.x, p.pos.z, p.pos.y); - p.mantle = { t: 0, dur: 0.22 + 0.13 * sobe, x0: p.pos.x, y0: p.pos.y, z0: p.pos.z, x1: alvo.x, y1: alvo.y, z1: alvo.z }; - this.sfx.jump(); - } - } // auto-fire (ak/m4/mp5) enquanto o botão está segurado if (WEAPONS[p.weapon].auto && this.mouseDown0 && p.alive) this._tryShoot(); this.bloom = Math.max(0, (this.bloom || 0) - dt * 1.8); @@ -5183,9 +4930,6 @@ export class Game { this.camera.rotation.set(p.pitch + p.recoilP, p.yaw, 0); // footsteps + view bob const moving = sp > 0.6 && p.grounded; - const running = moving && !p.scoped && p.crouchF < 0.2 && slowMul === 1 && sp >= maxSp * 0.88; - this._updateMotocaCharge(dt, running); - this._recordRoutePoint(moving); if (moving) { p.stepPhase += dt * sp * 1.6; const prev = Math.sin(p.stepPhase - dt * sp * 1.6), now = Math.sin(p.stepPhase); @@ -5208,13 +4952,6 @@ export class Game { this.camera.fov += Math.sign(tFov - this.camera.fov) * Math.min(stepFov, Math.abs(tFov - this.camera.fov)); this.camera.updateProjectionMatrix(); } else { this._fovFrom = undefined; this._fovTo = tFov; } - // PUNCH: pulso de zoom por disparo, decai exponencial. Canal próprio (camera.zoom): - // não disputa com a rampa de FOV do ADS acima nem com o recuo medido do vm-kick-sim. - if (this._punchF > 0.001) { - this._punchF *= Math.exp(-PUNCH_DECAY * dt); - const z = 1 + PUNCH_ZOOM * Math.min(1, this._punchF); - if (Math.abs((this.camera.zoom || 1) - z) > 0.0004) { this.camera.zoom = z; this.camera.updateProjectionMatrix(); } - } else if (this._punchF) { this._punchF = 0; this.camera.zoom = 1; this.camera.updateProjectionMatrix(); } // LUNETA (G3-R1 — conserto da "faixa preta"). A máscara é um overlay circular de borda // escura cuja opacidade acompanha o progresso do zoom (smoothstep). O que quebrava antes // não era a máscara e sim a ORDEM: arma e crosshair sumiam no frame do clique, enquanto a @@ -5367,6 +5104,7 @@ export class Game { if (wg) poseToWeapon(this.vm.arms, wg, p.weapon); } if (VMLAB) this._vmlabFrame(p, a); // ?vmlab=1: troca pelo viewmodel do editor (isolado) + this._updateReplayCam(dt); } // piscina_treta ground weapons: anyone who runs over one grabs it (CS-1.6 style). // The gun vanishes and respawns after PICKUP_RESPAWN. No-op on maps without @@ -5530,9 +5268,7 @@ export class Game { if (mode === 'awp') return 'awp'; if (mode === 'knife') return 'knife'; if (mode === 'pistols') return Math.random() < 0.5 ? 'pistol' : 'deagle'; - const pool = ['awp', 'ak', 'm4', 'mp5', 'shotgun', 'deagle', 'm92', 'akm', 'md97', - 'carbine', 'm400', 'mosin', 'rem700', 'lmg', 'scar', 'g3', 'tavor', 'famas', 'uzi', 'p90', 'revolver38']; - return pool[(Math.random() * pool.length) | 0]; + return BOT_WEAPON_POOL[(Math.random() * BOT_WEAPON_POOL.length) | 0]; } // Modo restrito não tem pickup de outra arma no mapa, então reserva finita = partida // acabada quando zera. Fica infinita a RESERVA, não o pente: a recarga segue cobrando. @@ -5647,11 +5383,6 @@ export class Game { _spawnY(x, z) { return this.world && this.world.groundHeightAt ? this.world.groundHeightAt(x, z) : 0; } - _spawnYaw(spawn, team, bot = false) { - if (spawn && Number.isFinite(spawn.yaw)) return spawn.yaw; - // Compatibilidade com mapa antigo sem yaw: preserva as convenções anteriores. - return team === 'E' ? (bot ? 0 : Math.PI) : (bot ? Math.PI : 0); - } _pickSpawn(team) { const list = this.world.spawns[team] || []; if (!list.length) return { x: 0, z: 0 }; @@ -5677,18 +5408,13 @@ export class Game { } _respawnPlayer() { const p = this.player; - /* MODO ARENA: aplica a troca de time antes de escolher o spawn. */ - if (this.arena && p._switchTeam) { - p.team = p._switchTeam; p._switchTeam = null; - this.sfx.general('headshot'); // sting de troca - } const s = this._pickSpawn(p.team); p.pos.set(s.x, this._spawnY(s.x, s.z), s.z); p.vel.set(0, 0, 0); p.hp = 100; p.alive = true; p.crouchF = 0; p._lifeDmg = 0; if (this._deathPanel) this._deathPanel.innerHTML = ''; // painel de morte não vaza pra vida nova p.protUntil = this.time + SPAWN_PROT; - p.yaw = this._spawnYaw(s, p.team, false); p.pitch = 0; + p.yaw = p.team === 'E' ? Math.PI : 0; p.pitch = 0; // Top off the CURRENT loadout's mags (primary could be any weapon now, not just AWP). // #268: em modo arma-única só recarrega slot PERMITIDO pelo modo (pistola não sai de // 0/0 no SÓ AWP), e quem morreu com arma proibida renasce com a arma do modo. @@ -5741,7 +5467,7 @@ export class Game { this.el.radioLog.appendChild(log); setTimeout(() => log.remove(), 3600); while (this.el.radioLog.children.length > 3) this.el.radioLog.firstChild.remove(); - try { this.sfx.characterVoice(b.def?.id, 'radio', { fallbackFaction: this._voiceKey(b.team) }); } catch {} + try { this.sfx.radioVoice(this._voiceKey(b.team)); } catch {} } /* ===================== MARCADOR DE TIME (halo + chevron) ===================== Dono: "ia ser legal se tivesse um halo no chão, ou uma seta em cima deles mostrando que @@ -5852,8 +5578,6 @@ export class Game { g.position.y = b.pos.y + Math.max(-0.6, 0 - b.deadT * 0.3); } if (this.time >= b.respawnAt && (this.state === 'live')) { - /* MODO ARENA: bot troca de time antes do respawn. */ - if (this.arena && b._switchTeam) { b.team = b._switchTeam; b._switchTeam = null; } const s = this._pickSpawn(b.team); // mesmo critério de segurança do jogador b.pos.set(s.x, this._spawnY(s.x, s.z), s.z); b.hp = 100; b.alive = true; /* RENASCER NO MESMO PIXEL: o _pickSpawn devolve o ponto MAIS SEGURO, e ele é o mesmo @@ -5867,7 +5591,7 @@ export class Game { b.mag = (WEAPONS[b.weapon] && WEAPONS[b.weapon].mag) || 30; b.aimErr = 0.2; b.burst = 0; b.alertUntil = 0; b._hurtAt = 0; b.reloadUntil = 0; b.focusUntil = 0; b._spinAcc = 0; b._spinAt = 0; b._sideUntil = 0; // estado de mira/anti-pirueta da vida anterior - b.target = null; b.path = null; b.yaw = this._spawnYaw(s, b.team, true); + b.target = null; b.path = null; b.yaw = b.team === 'E' ? 0 : Math.PI; b.laneX = undefined; b.roamUntil = 0; // re-sorteia a coluna A CADA VIDA -> rotas variam (não "sempre a mesma") b._banNodes = null; b._unreach = null; b._escapeUntil = 0; b._jukeAt = 0; // limpa estado de rota/juke da vida anterior (G2-R6A) // rumo suavizado e turno de duelo também são estado de vida: nascer com o _hdg da @@ -6751,7 +6475,6 @@ export class Game { const hw = this.ray.intersectObjects(this.world.occluders, false)[0]; if (hw && hw.distance < tdist - 0.4) hit = false; // parede na frente: bala morre lá const end = hit ? teye : (hw ? hw.point : from.clone().add(dir.clone().multiplyScalar(120))); - this.world.ambience?.onShot(from, end); if (hit) { let dmg = (Wb.dmg || 30) * (Wb.pellets ? Math.min(Wb.pellets, 6) * 0.55 : 1); const fo = DMG_FALLOFF[bcls]; @@ -6928,7 +6651,6 @@ export class Game { cada um"). O brasão é o MESMO arquivo que estampa a bandeira CTF (img/brasoes/), lido pela letra da facção que ocupa o lado — nunca pelo lado cru (a lição do _factionOf: lado 'B' ≠ facção 'B' por acidente de letra). */ - const coluna = (side) => { const linhas = [...this.combatants].filter(c => c.team === side).sort(rank).map((c, i) => { const score = Math.max(0, c.kills * 100 + (c.captures || 0) * 250 - c.deaths * 20); @@ -7075,6 +6797,11 @@ export class Game { /* ================= main update ================= */ update(dt, render = true) { if (this.paused) return; + // Hit-stop: scale dt during replay cam slowmo phase (uses wall-clock time, not game time) + if (this._replayCam) { + const wallT = this._replayCam._wallT = (this._replayCam._wallT || 0) + dt; + if (wallT < REPLAY_SLOWMO_DUR) dt *= REPLAY_SLOWMO; + } this.time += dt; if (this.state === 'countdown' && this.time >= this.stateUntil) { this.state = 'live'; @@ -7140,9 +6867,6 @@ export class Game { r.autoClear = true; } this._tickDolly(dt); - this.world.ambience?.update(dt, this.player.pos); - if (!this.soundscape && this.world.sound) this.soundscape = createSoundscape(this.sfx, this.world.sound); - this.soundscape?.update(dt, this.player.pos); this.world.update?.(dt, this.time); } @@ -7180,8 +6904,6 @@ export class Game { this.el.scoreboard.classList.add('hidden'); this.el.vignette.style.opacity = 0; if (this._dolly) { this._dolly.renderer.dispose(); this._dolly.canvas.remove(); this._dolly = null; } - this.world.ambience?.dispose(); - this.soundscape?.dispose(); this.soundscape = null; this.scene.traverse(o => { if (o.geometry) o.geometry.dispose(); }); this.scene.clear(); } diff --git a/public/js/glbchars.js b/public/js/glbchars.js index 1e2bef306..c0dfc943e 100644 --- a/public/js/glbchars.js +++ b/public/js/glbchars.js @@ -39,21 +39,12 @@ export const GLB_CHARS = new Set([ // os outros 8 são GLBs Mint riggados offline (tools/rig-from-donor.mjs, esqueleto do mst) // com clips retargetados em models/anims// (tools/retarget-glb.mjs). 'mandrake', 'raul', 'oakley', 'criarj', 'chave', 'funkraiz', 'trapfunk', 'fluxo', 'ostentacao', - // Mítico (6ª facção). Os três raws Meshy foram removidos; o elenco volta a - // apontar somente para malhas PBR. Bandeirante usa rig Meshy provisório, ainda em revisão. - 'mariabonita', 'saci', 'lampiao', 'lobisomem', 'bandeirante', 'cuca', 'curupira', 'boto', 'zumbi', - // Novas facções — fatias verticais da spec 0002; a facção só fica ready com 8/8. - 'camera-roxa', 'microfonildo', 'programador-virado', 'designer-ux', 'lenda-lanhouse', 'motoca-cachorro-loko', 'doidinho-bairro', - 'profeta-calcada', 'gilbomes', ]); -/* SET MANTIDO À MÃO: personagem fora daqui cai no procedural E some das réguas, porque a - `select-inflate.mjs` filtra o elenco por `GLB_CHARS`. Divergência com characters.js é caso de portão. */ - // Mascotes de braços-toco: a mão de apoio via IK vira uma mão gigante flutuando // (caso do Dollynho na tela de seleção). Neles, a mão L segue a pose do clipe. // Exportado para a select-mount.mjs: nesses, MÃO-L não se aplica (é decisão, não defeito). -export const IK_L_SKIP = new Set(['dollynho', 'gotinha', 'et', 'canarinho', 'cuca']); +export const IK_L_SKIP = new Set(['dollynho', 'gotinha', 'et', 'canarinho']); const STATES = ['idle', 'walk', 'run', 'shoot', 'death', 'crouch', 'crouchwalk', 'jump']; // Clipes OPCIONAIS: 1 mão (pistolas) + andando atirando. Se o arquivo não existir, @@ -120,19 +111,6 @@ const _num3 = (s, d) => { const p = (s || '').split(',').map(Number); return p.l const GUN_POS = _num3(qp.get('gunpos'), [0.02, 0.02, 0.04]); const GUN_ROT = _num3(qp.get('gunrot'), [90, 0, 0]).map((d) => d * Math.PI / 180); const GUN_SCALE = parseFloat(qp.get('guns')) || 1.0; -const LOBI_HAND_R = _num3(qp.get('lobirh'), [0, 0, 0]).map((d) => d * Math.PI / 180); -const LOBI_HAND_L = _num3(qp.get('lobilh'), [0, 0, 0]).map((d) => d * Math.PI / 180); -const LOBI_CURL_Q = parseFloat(qp.get('lobcurl')); -const LOBI_CURL = Number.isFinite(LOBI_CURL_Q) ? LOBI_CURL_Q : 0.7; -const LOBI_CURL_R = _num3(qp.get('lobcurlr'), [LOBI_CURL, 0, 0]); -const LOBI_CURL_L = _num3(qp.get('lobcurll'), [LOBI_CURL, 0, 0]); -// Rigs Meshy de palma plana: o curl combina duas falanges + compactação distal. No Programador -// 0,65 estourava a régua (P99=0,890), por isso 0,20. Régua: select-mount/select-inflate. -const CHAR_GRIP_CURL = new Map([ - ['programador-virado', 0.20], - ['motoca-cachorro-loko', 0.65], - ['doidinho-bairro', 0.65], -]); // Armas que precisam de +180° só na 3ª pessoa (mount). VAZIO desde 04/08: o flip da p90 // era da época de outro GLB; o modelo atual já nasce com o cano em +Z e o flip a deixava // de coronha pra frente (visto pelo dono na tela de seleção; A/B por figura no scratchpad @@ -157,11 +135,6 @@ const TP_MOUNT_LIVE = qp.get('tpmountlive') !== '0'; const _tpc = (qp.get('tpcarry') || '').split(',').map(Number); const TP_CARRY_PITCH = ((_tpc.length === 2 && !isNaN(_tpc[0]) ? _tpc[0] : -6)) * Math.PI / 180; // cano levemente pro chão (porte) const TP_CARRY_YAW = ((_tpc.length === 2 && !isNaN(_tpc[1]) ? _tpc[1] : 4)) * Math.PI / 180; // levemente cruzando o corpo -// BUG-47: no porte global de 4° a P90 (0,52 m) projetava só 0,110 m no 3:2 — abaixo dos -// 0,178 m da M4 aprovada. Yaw só do mount visual do Doidinho; roster, escala e balística intactos. -const TP_CHAR_CARRY_YAW = new Map([ - ['doidinho-bairro', -18], -]); const TP_CLEAR = parseFloat(qp.get('tpclear')) || 0.06; // folga mínima entre o grip e a superfície do corpo (m) // O empurrão só entra quando a palma está ENTERRADA de verdade. Medido: humano típico // tem a palma 3-10 cm dentro do volume do quadril na pose de idle (braço encostado no @@ -306,11 +279,14 @@ const torsoRadiusAt = (prof, y) => { // Preload shared clips + base meshes for the given character ids. Safe to call once // before a match; already-loaded assets are skipped. Failures are swallowed per-asset // so a missing model just falls back to the box mesh. -export async function preloadCharacterAssets(ids) { +export async function preloadCharacterAssets(ids, opts = {}) { + /* Armas FORA do bootstrap de clipes: a 1ª chamada é a da tela de carregamento, e prender as + armas ao `if (!_clips)` fazia a partida herdar a lista errada (ou as 26). Idempotente. */ + const armas = preloadWeapons(opts.weapons); if (!_clips) { _clips = {}; await Promise.all([ - preloadWeapons(), // real weapon GLBs (mounts fall back to box if missing) + armas, // Pack compartilhado em 1 GLB MESCLADO (tools/merge-anims.mjs): os 11 requests de // clipe viram 1. O THREE amarra o clipe no esqueleto pelo NOME do osso, então // basta ler as animações nomeadas. Se o mesclado faltar (deploy velho, ?animdir= @@ -337,7 +313,7 @@ export async function preloadCharacterAssets(ids) { ]); })(), ]); - } + } else await armas; const wanted = [...new Set(ids)].filter((id) => GLB_CHARS.has(id) && !_base.has(id)); await Promise.all(wanted.map(async (id) => { try { @@ -390,9 +366,6 @@ export function buildCharacterModel(def, opts = {}) { const template = _base.get(def.id); if (!template || !_clips) return null; const withWeapon = opts.weapon !== false; // menu showcase passes weapon:false - // A ficha do Bandeirante pede mosquete sem óptica. A arma competitiva continua sendo - // `mosin`; só o modelo de corpo/seleção troca, sem inventar balística ou slot novo. - const weaponId = def.id === 'bandeirante' ? 'mosquete' : (opts.weaponId || 'awp'); const model = skeletonClone(template); @@ -400,21 +373,9 @@ export function buildCharacterModel(def, opts = {}) { // Update world matrices first so the bounding box reflects the real transforms. model.updateMatrixWorld(true); const bbox = new THREE.Box3().setFromObject(model); - - /* ESTATURA É ANATÔMICA, NÃO É BBOX (régua: tools/eval/char-escala-check.mjs): normalizar - por bbox desconta adereço do corpo. Marco = osso `head_end`; rig sem ele cai no bbox e AVISA. */ - let cranioRaw = null; - model.traverse((o) => { - if (cranioRaw === null && o.isBone && /^(mixamorig)?head_?end$/i.test(o.name)) { - cranioRaw = o.getWorldPosition(new THREE.Vector3()).y; - } - }); - const hBbox = bbox.max.y - bbox.min.y || 1; - let h = hBbox; - if (cranioRaw !== null && cranioRaw - bbox.min.y > 1e-3) h = cranioRaw - bbox.min.y; - else console.warn(`[glbchars] ${def.id}: sem osso head_end utilizável — estatura caiu no bbox (adereço entra na conta).`); + const h = bbox.max.y - bbox.min.y || 1; const s = TARGET_HEIGHT / h; - if (qp.get('chartune')) console.log(`[glbchars] ${def.id} rawH=${h.toFixed(3)} bboxH=${hBbox.toFixed(3)} adereco=${(hBbox - h).toFixed(3)} min.y=${bbox.min.y.toFixed(3)} scale=${s.toFixed(3)}`); + if (qp.get('chartune')) console.log(`[glbchars] ${def.id} rawH=${h.toFixed(3)} min.y=${bbox.min.y.toFixed(3)} scale=${s.toFixed(3)}`); model.scale.setScalar(s); model.position.y = -bbox.min.y * s; model.rotation.y = FACING_OFFSET; @@ -425,9 +386,6 @@ export function buildCharacterModel(def, opts = {}) { const rimCol = charRimColor(def); model.traverse((o) => { if (!o.isMesh) return; - /* PROP RÍGIDO NUNCA ABSORVE TIRO (spec 0002 §8): o hitscan raycasta o grupo INTEIRO do - bot, então malha não-skinnada em socket viraria hitbox de graça. Mesmo mecanismo da sombra de contato em characters.js. */ - if (!o.isSkinnedMesh) o.raycast = () => {}; o.castShadow = true; // receiveShadow: sem isto a sombra do sol NUNCA escurecia o personagem — parte do // motivo de ele ler como adesivo. Quem impede que essa sombra o faça SUMIR é o piso @@ -457,35 +415,11 @@ export function buildCharacterModel(def, opts = {}) { // Head hitbox: an invisible (unrendered) but raycastable box tracked to the head bone // each frame, so headshots stay accurate through the animation. let headBone = null; - model.traverse((o) => { if (o.isBone && !headBone && /^(mixamorig)?head$/i.test(o.name)) headBone = o; }); - if (!headBone) model.traverse((o) => { if (o.isBone && !headBone && /head/i.test(o.name)) headBone = o; }); - - /* CAIXA DE HEADSHOT DO CRÂNIO DESTE PERSONAGEM (régua: char-escala-check.mjs): sai de - `neck` -> `head_end`; largura = 0,747 × altura, ancorada na mediana do elenco (0,348 m → 0,26 de hoje). */ - let caixaH = 0.30, caixaW = 0.26, headOffY = 0; - if (headBone) { - group.updateMatrixWorld(true); - const pego = (re) => { - let b = null; - model.traverse((o) => { if (!b && o.isBone && re.test(o.name)) b = o; }); - return b ? group.worldToLocal(b.getWorldPosition(new THREE.Vector3())).y : null; - }; - const yCranio = pego(/^(mixamorig)?head_?end$/i); - const yPescoco = pego(/^(mixamorig)?neck$/i); - const yOlho = group.worldToLocal(headBone.getWorldPosition(new THREE.Vector3())).y; - if (yCranio !== null && yPescoco !== null && yCranio - yPescoco > 1e-3) { - caixaH = yCranio - yPescoco; - caixaW = caixaH * 0.747; - headOffY = (yCranio + yPescoco) / 2 - yOlho; - } else { - console.warn(`[glbchars] ${def.id}: sem neck/head_end utilizáveis — hitbox de cabeça caiu no tamanho fixo antigo (headshot injusto neste personagem).`); - } - } + model.traverse((o) => { if (o.isBone && !headBone && /head/i.test(o.name)) headBone = o; }); const head = new THREE.Mesh( - new THREE.BoxGeometry(caixaW, caixaH, caixaW), + new THREE.BoxGeometry(0.26, 0.30, 0.26), new THREE.MeshBasicMaterial({ visible: false }), ); - head.userData.csHeadOffY = headOffY; group.add(head); // Rifle in the right hand: a scale-compensated mount parented to the hand bone so @@ -496,11 +430,15 @@ export function buildCharacterModel(def, opts = {}) { model.traverse((o) => { if (o.isBone && !rforeBone && /right.?forearm|r_forearm/i.test(o.name)) rforeBone = o; }); if (!handBone) model.traverse((o) => { if (o.isBone && !handBone && /hand/i.test(o.name)) handBone = o; }); if (handBone && withWeapon) { - const gun = weaponModel(weaponId) || buildRifle(); + /* O nome é o que torna a queda em caixa MEDÍVEL: sem ele a arma procedural some calada + dentro da cena e nenhuma régua enxerga (ARM2). */ + const glb = weaponModel(opts.weaponId || 'awp'); + const gun = glb || buildRifle(); + gun.name = glb ? 'arma-glb' : 'arma-caixa'; // Flip de 180° SÓ na 3ª pessoa p/ armas que ficam de costas no mount (o cano +Z da // weaponModel não bate com a mão nesses casos). O FP tem seu próprio ajuste (vmRotY), // então corrigimos aqui sem tocar no viewmodel já validado. (P90: usuário viu ao contrário.) - if (TP_FLIP_Y.has(weaponId)) gun.rotateY(Math.PI); + if (TP_FLIP_Y.has(opts.weaponId)) gun.rotateY(Math.PI); gunObj = gun; // Measure the weapon's authored (real-world) size in its own space, before parenting. gun.updateMatrixWorld(true); @@ -538,7 +476,7 @@ export function buildCharacterModel(def, opts = {}) { const ctrl = new CharController(mixer, actions, group, headBone, head, def.id, model); ctrl.shadow = shadow; // antes do settle loop abaixo (ctrl.update já a atualiza) // Arma de 1 mão (pistol/deagle/revolver38/knife): usa idle1h/walk1h quando carregados. - ctrl.oneHanded = ONE_HANDED.has(weaponId); + ctrl.oneHanded = !!(opts.weaponId && ONE_HANDED.has(opts.weaponId)); if (handBone && withWeapon) { // A pose tem que estar ASSENTADA antes de medir qualquer coisa: o mixer acabou de @@ -563,11 +501,7 @@ export function buildCharacterModel(def, opts = {}) { measurePalmLocal, que perdia os vértices dos dedos na média da palma. Agora: junta TODOS os ossos com esse nome e prefere os que têm peso de skin. */ const curlsR = [], curlsL = []; - model.traverse((o) => { - if (!o.isBone) return; - if (o.name === 'Curl_R' || o.name === 'Curl_R_Tip') curlsR.push(o); - if (o.name === 'Curl_L' || o.name === 'Curl_L_Tip') curlsL.push(o); - }); + model.traverse((o) => { if (o.isBone) { if (o.name === 'Curl_R') curlsR.push(o); if (o.name === 'Curl_L') curlsL.push(o); } }); const comPeso = (lista) => { if (lista.length < 2) return lista; let sk = null; @@ -597,13 +531,10 @@ export function buildCharacterModel(def, opts = {}) { // medidos por vértice, probe-muzzle 05/08) com pitch pra baixo — sem isso o revólver // do bonzo aponta ~35° pro céu. No jogo o porte funcional fica intocado: bot mira // pra onde olha (funcional > identidade). A/B: scratchpad sel_now × sel_fix. - const cd = opts.preview ? (ONE_HANDED.has(weaponId) ? [4, 26] : [-14, 40]) : null; - const charCarryYaw = _tpc.length === 2 && !isNaN(_tpc[1]) - ? TP_CARRY_YAW - : (TP_CHAR_CARRY_YAW.get(def.id) ?? 4) * Math.PI / 180; + const cd = opts.preview ? (ONE_HANDED.has(opts.weaponId) ? [4, 26] : [-14, 40]) : null; const carry = new THREE.Quaternion().setFromEuler(cd ? new THREE.Euler(cd[0] * Math.PI / 180, cd[1] * Math.PI / 180, 0, 'YXZ') - : new THREE.Euler(TP_CARRY_PITCH, charCarryYaw, 0, 'YXZ')); + : new THREE.Euler(TP_CARRY_PITCH, TP_CARRY_YAW, 0, 'YXZ')); const bodyQ = model.getWorldQuaternion(new THREE.Quaternion()); const desired = bodyQ.multiply(carry); const handQ = handBone.getWorldQuaternion(new THREE.Quaternion()); @@ -625,12 +556,7 @@ export function buildCharacterModel(def, opts = {}) { Kill-switch: ?tpmountlive=0 volta ao comportamento congelado. */ let rArmBone = null; model.traverse((o) => { if (o.isBone && !rArmBone && o.name === 'RightArm') rArmBone = o; }); - if (TP_MOUNT_LIVE) ctrl.tpMount = { - mount, handBone, model, carry, palmLocal: null, rArm: rArmBone, rFore: rforeBone, - handAdjust: def.id === 'lobisomem' - ? new THREE.Quaternion().setFromEuler(new THREE.Euler(...LOBI_HAND_R)) - : null, - }; + if (TP_MOUNT_LIVE) ctrl.tpMount = { mount, handBone, model, carry, palmLocal: null, rArm: rArmBone, rFore: rforeBone }; // 2) POSIÇÃO: no centro medido da PALMA, não na origem do osso (que é o PULSO). // É a diferença entre "a mão segura a arma" e "a arma flutua perto da mão" (C7). const palmLocal = measurePalmLocal(model, handBone, curlRs); @@ -670,7 +596,7 @@ export function buildCharacterModel(def, opts = {}) { } // Grip curl: close the fingers onto the grip (the auto-skinned curl bones). // Two-handed weapons curl both hands; one-handed only the grip (right) hand. - const twoHanded = !ONE_HANDED.has(weaponId); + const twoHanded = !ONE_HANDED.has(opts.weaponId || 'awp'); /* CURL DOS DEDOS: era 0,5 rad CRAVADO nas duas mãos, para as 26 armas. O curl é o quanto os dedos fecham em volta do PUNHO da arma, então o valor certo é função da GROSSURA do que a mão agarra — um punho de pistola e o guarda-mão de uma @@ -678,8 +604,7 @@ export function buildCharacterModel(def, opts = {}) { fina ou os dedos atravessam a arma grossa; nos dois casos lê como "mão solta". Aqui o ângulo sai da própria arma: `gripPoints` dá o ponto do guarda-mão e o weaponModel dá a caixa, então a espessura no grip é MEDIDA e não tabelada. - Faixa geral 0,35-0,80 rad: abaixo disso a mão não fecha, acima pode entrar na palma. - Rigs com bind excepcional usam CHAR_GRIP_CURL, medido por A/B servido. + Faixa 0,35-0,80 rad: abaixo disso a mão não fecha, acima os dedos entram na palma. RESSALVA HONESTA: sem os GLB de personagem nesta árvore não deu pra conferir o resultado em imagem. A faixa é conservadora e contém o 0,5 antigo, então o pior caso é ficar igual ao que já estava. */ @@ -692,24 +617,10 @@ export function buildCharacterModel(def, opts = {}) { // mão humana fecha ~0,8 rad em volta de 3 cm e ~0,35 rad em volta de 9 cm return Math.max(0.35, Math.min(0.80, 0.80 - (esp - 0.03) * (0.45 / 0.06))); }; - const curl = def.id === 'lobisomem' - ? LOBI_CURL - : (CHAR_GRIP_CURL.get(def.id) ?? (gunObj ? curlPara(gunObj) : 0.5)); + const curl = gunObj ? curlPara(gunObj) : 0.5; // fecha TODOS os ossos de curl com peso (nos 18 rigs transplantados eles vêm em par) - for (const b of curlRs) { - if (def.id === 'lobisomem') { - b.rotation.x += LOBI_CURL_R[0]; - b.rotation.y += LOBI_CURL_R[1]; - b.rotation.z += LOBI_CURL_R[2]; - } else b.rotation.x += curl; - } - if (twoHanded) for (const b of curlLs) { - if (def.id === 'lobisomem') { - b.rotation.x += LOBI_CURL_L[0]; - b.rotation.y += LOBI_CURL_L[1]; - b.rotation.z += LOBI_CURL_L[2]; - } else b.rotation.x += curl; - } + for (const b of curlRs) b.rotation.x += curl; + if (twoHanded) for (const b of curlLs) b.rotation.x += curl; // IK da mão de apoio (FASE 2): em armas de 2 mãos, o CharController trava a palma L // no guarda-mão depois de cada mixer.update — vale pra idle/walk dos bots E pro // preview da tela de seleção (mesmo ctrl.update). Posicional apenas: os clipes já @@ -717,18 +628,8 @@ export function buildCharacterModel(def, opts = {}) { if (twoHanded && lhandBone) { let lArm = null, lFore = null; model.traverse(o => { if (o.isBone) { if (o.name === 'LeftArm') lArm = o; if (o.name === 'LeftForeArm') lFore = o; } }); - const gp = gripPoints(weaponId); - if (lArm && lFore && gp.fore && gunObj && !IK_L_SKIP.has(def.id)) { - const fore = gp.fore.clone(); - if (def.id === 'lobisomem') fore.y -= 0.03; - ctrl.ikL = { - chain: [lArm, lFore], end: lhandBone, - endOffset: measurePalmLocal(model, lhandBone, curlLs), gun: gunObj, fore, - handAdjust: def.id === 'lobisomem' - ? new THREE.Quaternion().setFromEuler(new THREE.Euler(...LOBI_HAND_L)) - : null, - }; - } + const gp = gripPoints(opts.weaponId || 'awp'); + if (lArm && lFore && gp.fore && gunObj && !IK_L_SKIP.has(def.id)) ctrl.ikL = { chain: [lArm, lFore], end: lhandBone, endOffset: measurePalmLocal(model, lhandBone, curlLs), gun: gunObj, fore: gp.fore.clone() }; } } return { group, parts: { head }, isGLB: true, mixer, ctrl }; @@ -877,9 +778,6 @@ class CharController { if (this.headBone) { this.group.updateMatrixWorld(true); this.head.position.copy(this.group.worldToLocal(this.headBone.getWorldPosition(_v))); - // O osso `Head` fica na BASE do crânio; a caixa cobre o crânio INTEIRO (meio de - // [pescoço, head_end]) — deslocamento fixo do rig, medido uma vez na construção. - this.head.position.y += this.head.userData.csHeadOffY || 0; } // Sombra de contato: reage ao estado do corpo. No pulo ela ABRE e desbota (penumbra // que cresce com a distância do contato, critério A4); agachado ela FECHA e escurece @@ -902,16 +800,6 @@ class CharController { // em osso e a conta aqui depende da matriz de mundo já resolvida. if (this.tpMount && !this.dead) { const t = this.tpMount; - /* Ajuste de empunhadura: girar o osso da mão gira o vetor pulso→palma; guarda a âncora - mundial antes e recalcula a posição local do mount depois — só a mão muda de pose. */ - if (t.handAdjust) { - t.handBone.updateWorldMatrix(true, false); - _v.copy(t.palmLocal || t.mount.position); - t.handBone.localToWorld(_v); - t.handBone.quaternion.multiply(t.handAdjust); - t.handBone.updateWorldMatrix(true, false); - t.mount.position.copy(t.handBone.worldToLocal(_v)); - } t.handBone.updateWorldMatrix(true, false); t.model.getWorldQuaternion(_gq); _wq.copy(_gq).multiply(t.carry); // orientação desejada do cano (mundo) @@ -928,16 +816,13 @@ class CharController { // converge no quadro seguinte — o assentamento da seleção roda 60 quadros. if (t.palmLocal && t.rArm && t.rFore && TP_FRONT_MIN > -Infinity && /^(idle|idle1h|crouch)$/.test(this.curName || '')) { - // Com handAdjust, mount.position já contém a compensação da âncora mundial; medir pelo - // palmLocal antigo reintroduziria o arco removido acima e empurraria a arma a cada quadro. - const anchorLocal = t.handAdjust ? t.mount.position : t.palmLocal; - _v.copy(anchorLocal); + _v.copy(t.palmLocal); t.handBone.localToWorld(_v); t.model.worldToLocal(_v); if (_v.z < TP_FRONT_MIN) { _v.z = TP_FRONT_MIN; t.model.localToWorld(_v); - solveCCDIK([t.rArm, t.rFore], t.handBone, _v, { iterations: 6, endOffset: anchorLocal }); + solveCCDIK([t.rArm, t.rFore], t.handBone, _v, { iterations: 6, endOffset: t.palmLocal }); t.handBone.updateWorldMatrix(true, false); t.model.getWorldQuaternion(_gq); _wq.copy(_gq).multiply(t.carry); @@ -954,7 +839,6 @@ class CharController { _ikTgt.copy(ik.fore); ik.gun.localToWorld(_ikTgt); solveCCDIK(ik.chain, ik.end, _ikTgt, { iterations: 8, endOffset: ik.endOffset }); - if (ik.handAdjust) ik.end.quaternion.multiply(ik.handAdjust); } } } diff --git a/public/js/i18n.js b/public/js/i18n.js index 13584981c..fb8b9ac6d 100644 --- a/public/js/i18n.js +++ b/public/js/i18n.js @@ -90,6 +90,12 @@ const DICT = { 'OFICIAL': 'OFFICIAL', 'por': 'by', // descrições de categoria da tela de mapas (a chave é o texto PT exato do CAT_DESC) 'O acervo inteiro: oficial e comunidade, arena e cidade.': 'The whole roster: official and community, arena and city.', + // as duas abas que sobraram do #368 trocaram o texto de categoria + 'Mapas oficiais da casa.': 'Maps made in-house.', + 'O acervo inteiro, do mais jogado ao menos jogado.': 'The whole roster, most played first.', + 'PARTIDA': 'MATCH', 'PARTIDAS': 'MATCHES', + 'Mapas feitos pela comunidade.': 'Maps made by the community.', + 'OFICIAIS': 'OFFICIAL', 'Combate fechado e simétrico — o duelo de angulação clássico.': 'Tight, symmetric combat — the classic angle duel.', 'Verticalidade de laje, beco e sombra: quem domina o alto dita o round.': 'Rooftop verticality, alley and shade: who owns the high ground runs the round.', 'Marcos do Brasil em escala de treta: concreto, calçada e linha reta.': 'Brazilian landmarks at treta scale: concrete, sidewalk and straight lines.', @@ -310,11 +316,126 @@ const DICT = { 'KILLS': 'KILLS', 'MORTES': 'DEATHS', 'JOGADOR': 'PLAYER', 'CAP.': 'CAP.', 'CORO SOLTO — PLACAR': 'CORO SOLTO — SCOREBOARD', 'A treta continua sem você. Por enquanto.': 'The fight goes on without you. For now.', + /* --- varredura de 21/08: as sobras PT que apareciam no meio do EN (tela 04 e vizinhas). + Personagem, facção e nome de mapa continuam PT nos dois idiomas — é sabor, decisão do dono. */ + // boot / erro + 'CARREGANDO MAPA': 'LOADING MAP', + 'A TRETA DEU UMA PAUSA': 'THE FIGHT TOOK A BREAK', + 'A ARENA NÃO ABRIU': 'THE ARENA DID NOT OPEN', + 'Alguma coisa travou durante o carregamento. O erro já foi registrado automaticamente, sem pedir senha.': + 'Something jammed while loading. The error was logged automatically, no password needed.', + 'RELATÓRIO LOCAL': 'LOCAL REPORT', + 'TENTAR DE NOVO': 'TRY AGAIN', + 'REPORTAR ERRO': 'REPORT ERROR', + 'Se quiser, confirme o envio pelo botão acima.': 'If you like, confirm the submission with the button above.', + 'DICA': 'TIP', 'CORRIDA': 'RUN', + // aviso de desktop + 'Este jogo foi feito para': 'This game was built for', + '(mouse + teclado).': '(mouse + keyboard).', + 'Jogar num PC ou notebook é': 'Playing on a PC or laptop is', + 'recomendado': 'recommended', + '. No celular a treta não flui.': '. On a phone the fight does not flow.', + // menu principal / perfil + 'RANKING GLOBAL': 'GLOBAL LEADERBOARD', + 'MAPA DA TRETA': 'THE MAP', + 'SOBRE': 'ABOUT', + 'ENVIE SEU FEEDBACK': 'SEND YOUR FEEDBACK', + 'EDITAR PERFIL': 'EDIT PROFILE', + 'NÍVEL 1': 'LEVEL 1', + '▶ JOGAR': '▶ PLAY', + 'CONFIG.': 'SETTINGS', + 'SEU NICK': 'YOUR NICK', + 'REDES SOCIAIS (OPCIONAL)': 'SOCIAL LINKS (OPTIONAL)', + '+ REDE SOCIAL': '+ SOCIAL LINK', + '+ FOTO DE PERFIL': '+ PROFILE PHOTO', + 'É esse nome que aparece no killfeed. Escolha com carinho - a treta é pública.': + 'This is the name that shows up in the killfeed. Choose it well — the fight is public.', + 'ex: Zé do AWP': 'e.g. AWP Joe', + // tela 04 · escolha do mapa + '04 · ESCOLHA DO MAPA': '04 · PICK THE MAP', + 'Categorias de mapa': 'Map categories', + 'Opções da partida': 'Match options', + 'Mapa anterior': 'Previous map', 'mapa anterior': 'previous map', + 'Próximo mapa': 'Next map', 'próximo mapa': 'next map', + 'Ver mapa em tela cheia': 'View map full screen', + 'Escolher o mapa da partida': 'Pick the match map', + 'Escolher as armas da partida': 'Pick the match weapons', + 'Clique pra trocar entre ROUNDS e CAPTURE THE FLAG': 'Click to switch between ROUNDS and CAPTURE THE FLAG', + // fichas dos mapas que faltavam (as antigas já estavam acima) + 'Galpão de atacado em guerra: gôndolas apertadas, caixas de cobertura e o estacionamento disputado carrinho por carrinho.': + 'A wholesale warehouse at war: tight aisles, crates for cover and a parking lot contested cart by cart.', + 'Um parque de diversões em guerra de confete: carrossel no centro, roda-gigante, castelo colorido e três rotas de ataque.': + 'An amusement park in a confetti war: carousel at the center, ferris wheel, colorful castle and three attack routes.', + 'Duelo na cidade empoeirada: saloon, banco, carroças e tumbleweeds cruzando três rotas entre casas de madeira.': + 'A duel in the dusty town: saloon, bank, wagons and tumbleweeds crossing three routes between wooden houses.', + 'Rebelião no pátio: celas abertas, concreto gasto, guaritas e barricadas policiais entre três rotas de confronto.': + 'Yard riot: open cells, worn concrete, watchtowers and police barricades across three routes.', + 'Pronto-socorro lotado: salas de verdade, corredor em cruz e treta no fluorescente — 100% interno.': + 'A packed emergency room: real wards, a cross-shaped corridor and a fight under fluorescent light — fully indoors.', + 'Canteiro de obra eterna: terreno ondulado, buracos de escavação, tapumes e a treta do desvio de verba.': + 'An eternal construction site: uneven ground, dig pits, hoardings and the fight over the missing budget.', + // personagem / config / como jogar + 'personagem anterior': 'previous character', 'próximo personagem': 'next character', + 'Categorias de configuração': 'Settings categories', + 'Configurações': 'Settings', + 'Escolha sua região': 'Choose your region', + 'Liga/desliga as falas (memes)': 'Toggle the meme voice lines', + 'FECHAR': 'CLOSE', 'JOGABILIDADE': 'GAMEPLAY', + 'QUALIDADE GRÁFICA': 'GRAPHICS QUALITY', + 'SENSIBILIDADE DO MOUSE': 'MOUSE SENSITIVITY', + 'LIGADO': 'ON', 'DESLIGADO': 'OFF', + 'VOLUME GERAL': 'MASTER VOLUME', + 'FALAS DOS MEMES': 'MEME VOICE LINES', + 'COR DA MIRA': 'CROSSHAIR COLOR', + 'AJUDAR A TREINAR OS BOTS': 'HELP TRAIN THE BOTS', + '“Padrão ouro” liga sombras e neblina. Em notebook de reunião, escolha “Batata”.': + '“Gold standard” turns on shadows and fog. On a meeting laptop, pick “Potato”.', + 'PRÉVIA · FERRO VELHO DO ZÉ · PADRÃO OURO': 'PREVIEW · FERRO VELHO DO ZÉ · GOLD STANDARD', + 'RESTAURAR PADRÃO': 'RESTORE DEFAULTS', + 'APLICAR': 'APPLY', 'SALVAR': 'SAVE', + 'Estes stats ficam': 'These stats live', 'neste navegador': 'in this browser', + '. O ranking global está desligado enquanto o jogo está em alpha - volta quando o placar for confiável.': + '. The global leaderboard is off while the game is in alpha — it returns when the scoreboard is trustworthy.', + 'O CORO SOLTO ainda está em alpha. Se você curte a ideia, qualquer apoio ajuda a pagar servidor, domínio e as próximas melhorias.': + 'CORO SOLTO is still in alpha. If you like the idea, any support helps pay for the server, the domain and the next improvements.', + '🇧🇷 SOU DO BRASIL': '🇧🇷 I AM IN BRAZIL', + '🌐 FORA DO BRASIL': '🌐 OUTSIDE BRAZIL', + 'ABRIR PÁGINA DE APOIO': 'OPEN THE SUPPORT PAGE', + 'AWP / Pistola / Faca': 'AWP / Pistol / Knife', + 'CTRL ou C': 'CTRL or C', + 'Gire o celular na horizontal pra jogar': 'Turn your phone sideways to play', + 'com respawn: cada round dura 1:39 ou fecha quando um time chega no alvo de abates; o time com mais kills leva o round. Por padrão, vence quem ganhar 3 rounds (melhor de 5); o teto pode ser escolhido na tela de mapas.': + 'with respawn: each round lasts 1:39 or ends when a team hits the kill target; the team with more kills takes the round. By default the first to 3 rounds wins (best of 5); the cap can be chosen on the map screen.', + 'não tem cronômetro de round: a rodada acaba quando um time captura 3 bandeiras (ou domina todas de uma vez). Vence quem ganhar 2 rodadas (melhor de 3). A partida tem um limite de 8 minutos, que só aparece no HUD no último minuto.': + 'has no round clock: the round ends when a team captures 3 flags (or holds them all at once). First to 2 rounds wins (best of 3). The match has an 8-minute cap that only shows on the HUD in the final minute.', + // HUD / placar + 'PROTEGIDO': 'PROTECTED', + 'RECARREGANDO…': 'RELOADING…', + 'Z/X/V RÁDIO': 'Z/X/V RADIO', 'TAB PLACAR': 'TAB SCOREBOARD', 'M TROCAR DE TIME': 'M SWITCH TEAM', + 'CLIQUE PARA ATIVAR A MIRA': 'CLICK TO LOCK THE CROSSHAIR', + 'Se o navegador bloquear, clique de novo. Em iframe/preview, abra o jogo em aba própria.': + 'If the browser blocks it, click again. Inside an iframe/preview, open the game in its own tab.', + 'ELIMINADO': 'ELIMINATED', + 'CORO SOLTO - PLACAR': 'CORO SOLTO — SCOREBOARD', + 'SEGURAR PARA VER': 'HOLD TO VIEW', 'ATIVAR CURSOR': 'ENABLE CURSOR', + 'Armas e utilitários': 'Weapons and utilities', + 'Fumaça (tecla 4) · Frag (tecla 5)': 'Smoke (key 4) · Frag (key 5)', + // rodapé / links + 'Links do jogo': 'Game links', 'Menu principal': 'Main menu', 'Abrir seu perfil': 'Open your profile', + 'Discord do CORO SOLTO': 'CORO SOLTO Discord', 'Telegram do CORO SOLTO': 'CORO SOLTO Telegram', + 'Código no GitHub': 'Source on GitHub', }; +/* Índice por espaço NORMALIZADO: o mesmo parágrafo quebrado em duas linhas no + index.astro chega aqui com \n e indentação no meio. Sem isto, cada quebra de linha + do HTML vira uma sobra em PT no meio do EN (foi o que a varredura de 21/08 achou). */ +const norm = (s) => s.replace(/\s+/g, ' ').trim(); +const DICT_NORM = {}; +for (const k of Object.keys(DICT)) DICT_NORM[norm(k)] = DICT[k]; + export const tr = (s) => { if (LANG !== 'en' || typeof s !== 'string') return s; - return DICT[s] || DICT[s.trim()] || s; + return DICT[s] || DICT[s.trim()] || DICT_NORM[norm(s)] || s; }; /* Frases DINÂMICAS do jogo (game.js/main.js). PT inline como padrão — o jogo nunca @@ -380,13 +501,14 @@ export function translateDom(root) { for (const n of nos) { const t = n.textContent, tt = t.trim(); if (!tt) continue; - const en = DICT[tt]; + const en = DICT[tt] || DICT_NORM[norm(tt)]; if (en) n.textContent = t.replace(tt, en); } for (const el of root.querySelectorAll('[placeholder],[title],[aria-label]')) { for (const a of ['placeholder', 'title', 'aria-label']) { const v = el.getAttribute(a); - if (v && DICT[v.trim()]) el.setAttribute(a, DICT[v.trim()]); + const en = v && (DICT[v.trim()] || DICT_NORM[norm(v)]); + if (en) el.setAttribute(a, en); } } } diff --git a/public/js/loading3d.js b/public/js/loading3d.js index 6037ad409..332164149 100644 --- a/public/js/loading3d.js +++ b/public/js/loading3d.js @@ -81,7 +81,9 @@ export class LoadingCharacterStage { this.canvas.dataset.ready = '0'; if (!this.renderer) return; if (!this.cache.has(id)) { - await preloadCharacterAssets([id]); + /* Só a arma DESTE preview: sem a lista, o bootstrap puxava as 26 antes de a partida + existir — 7,5 MB e ~164 MB de VRAM na tela de carregamento. Régua: ARM1. */ + await preloadCharacterAssets([id], { weapons: [charWeapon(id)] }); if (token !== this.token || !this.active) return; const def = CHARACTERS.find((character) => character.id === id); const built = def && hasModel(id) diff --git a/public/js/main.js b/public/js/main.js index 477c1cb17..06dddff33 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -5,20 +5,16 @@ import { CHARACTERS, buildCharacter, charWeapon } from './characters.js'; import { preloadCharacterAssets, buildCharacterModel, hasModel, GLB_CHARS } from './glbchars.js'; import { preloadFPArms } from './fparms.js'; import { preloadMapProps } from './mapprops.js'; -import { preloadAmbientLife } from './ambientlife.js'; import { MAPS, MAP_IDS, DEFAULT_MAP, resolveMapId, mapaDaSessao } from './maps.js'; import { PALETA } from './paleta.js'; import { setHavanCarSeed } from './map_havan.js'; +import { preloadWeapons } from './weapons.js'; import { Sfx } from './audio.js'; -import { Game, confirmGate, CONFIRM_MAX_MS, pickMatchRoster } from './game.js'; +import { Game, confirmGate, CONFIRM_MAX_MS, pickMatchRoster, pickMatchWeapons } from './game.js'; import { VERSION } from './version.js'; import { LANG, resolveGeoLang, translateDom, tr, frase } from './i18n.js'; import { enableLightBloom } from './bloom.js'; import { enableStylize } from './stylize.js'; -import { FACTIONS } from './factions.js'; -/* Literal exigido pela régua UIR1 (redesign-check lê a declaração, não o uso); - a fonte dos nomes é factions.js — mantenha os dois em sincronia. */ -const FACTION_NAME = { E: 'TIME E', B: 'TIME B', U: 'TRIBOS URBANAS', C: 'PALHACOS', F: 'FUNKEIROS', M: 'MITICOS', N: 'NERDOLAS', R: 'PROFISSIONAIS DO CORRE', O: 'NOIAS', T: 'TV' }; import { resolveInspectionScreen } from './screenquery.js'; import { LoadingCharacterStage } from './loading3d.js'; @@ -143,14 +139,6 @@ sfx.onDuck = (amt, hold) => { m.volume = MENU_MUSIC_VOL * amt; setTimeout(() => { if (menuMusic && !musicFade && !menuMusic.paused) menuMusic.volume = MENU_MUSIC_VOL; }, hold * 1000 + 220); }; -sfx.onCharacterVoice = ({ characterId, event, text }) => { - if (event !== 'select') return; - const caption = $('char-voice-caption'); - if (!caption) return; - const character = CHARACTERS.find((entry) => entry.id === characterId); - caption.textContent = text ? `${character?.name || characterId}: “${text}”` : ''; - caption.classList.toggle('show', !!text); -}; const sfxReady = sfx.loadManifest(); /* ---------------- selected map ---------------- */ @@ -287,66 +275,13 @@ function rebuildMenuBackdrop() { menuScene = new THREE.Scene(); MAPS[currentMap].build(menuScene, textures); } -function menuProps(id) { - return [...MAP_PROPS, ...((MAPS[id] && MAPS[id].props) || [])]; -} -let _menuLoadSeq = 0; -function loadMenuBackdrop() { - const id = currentMap, seq = ++_menuLoadSeq; - return Promise.all([ - preloadMapProps(menuProps(id)), - preloadAmbientLife((MAPS[id] && MAPS[id].ambience) || []), - ]).then(() => { - // O jogador pode trocar de mapa enquanto o GLB baixa. Resultado velho não reconstrói - // a cena nova; a próxima chamada tem seu próprio preload e sequência. - if (seq === _menuLoadSeq && id === currentMap) rebuildMenuBackdrop(); - }); -} // The first backdrop is built before props load; rebuild once they're ready so the // menu shows the real Brasília landmarks too. Só então a splash libera a entrada. -loadMenuBackdrop().then(_splashSetReady).catch(_splashSetReady); +preloadMapProps(MAP_PROPS).then(() => { rebuildMenuBackdrop(); _splashSetReady(); }).catch(() => _splashSetReady()); /* ---------------- screens ---------------- */ -const CINE_SCREEN_META = Object.freeze({ - 'mobile-warning': { section: 'ACESSO', step: 'DESKTOP RECOMENDADO', progress: 4 }, - 'main-menu': { section: 'ABERTURA', step: 'ESCOLHA A TRETA', progress: 12 }, - 'map-screen': { section: 'PREPARAÇÃO', step: '01 · O PALCO', progress: 26 }, - 'team-select': { section: 'ESCALAÇÃO', step: '02 · O SEU LADO', progress: 44 }, - 'char-select': { section: 'ESCALAÇÃO', step: '03 · O PERSONAGEM', progress: 62 }, - 'settings-panel': { section: 'SISTEMA', step: 'AJUSTE A ARENA', progress: 18 }, - 'howto-panel': { section: 'ARQUIVO', step: 'MANUAL DE CAMPO', progress: 18 }, - 'ranking-panel': { section: 'ARQUIVO', step: 'PLACAR DA RUA', progress: 18 }, - 'feedback-panel': { section: 'CANAL ABERTO', step: 'MANDE O PAPO', progress: 18 }, - 'pause-menu': { section: 'INTERVALO', step: 'A TRETA ESPERA', progress: 76 }, - 'match-end': { section: 'DESFECHO', step: 'FIM DE RODADA', progress: 100 }, - 'support-panel': { section: 'CANAL ABERTO', step: 'APOIE A TRETA', progress: 18 }, -}); const screens = ['mobile-warning', 'main-menu', 'map-screen', 'team-select', 'char-select', 'settings-panel', 'howto-panel', 'ranking-panel', 'feedback-panel', 'support-panel', 'pause-menu', 'match-end']; -function applyCinematicScreen(id) { - if (!id || !CINE_SCREEN_META[id]) { - delete document.body.dataset.cineScreen; - return; - } - const meta = { ...CINE_SCREEN_META[id] }; - if (id === 'main-menu' && document.getElementById('menu-setup')?.classList.contains('open')) { - const profile = document.getElementById('menu-setup')?.dataset.step === 'profile'; - meta.section = profile ? 'IDENTIDADE' : 'PREPARAÇÃO'; - meta.step = profile ? 'SEU PERFIL' : '01 · A PARTIDA'; - meta.progress = profile ? 20 : 26; - } - if (id === 'team-select' && document.getElementById('team-select')?.dataset.step === 'enemy') { - meta.section = 'CONFRONTO'; meta.step = '04 · O ADVERSÁRIO'; meta.progress = 82; - } - document.body.dataset.cineScreen = id; - const section = document.getElementById('cine-section'); - const step = document.getElementById('cine-step'); - const progress = document.getElementById('cine-progress'); - if (section) section.textContent = meta.section; - if (step) step.textContent = meta.step; - if (progress) progress.style.width = `${meta.progress}%`; -} function show(id) { - applyCinematicScreen(id); for (const s of screens) document.getElementById(s).classList.toggle('hidden', s !== id); if (!id) for (const s of screens) document.getElementById(s).classList.add('hidden'); if (id !== 'char-select') pvStopVideo(); @@ -1120,12 +1055,15 @@ async function _startGame(team, charId, enemyFaction) { .map((d) => (typeof d === 'string' ? d : d.id)) .filter((id, i, a) => GLB_CHARS.has(id) && a.indexOf(id) === i); const _charsToLoad = _rosterGlb.length ? _rosterGlb : [...GLB_CHARS]; + /* Armas da partida sorteadas aqui pelo mesmo motivo do roster: as 26 custavam 164 MB de VRAM + e 7,5 MB de download numa partida que usa ~9. O resto chega em ocioso. Régua: ARM1. */ + const matchWeapons = pickMatchWeapons({ mode: settings.wpnMode || 'all', teamSize: Math.max(1, Math.min(8, settings.bots || 4)) }); + const _armasDaPartida = [...new Set([charWeapon(charId), ...matchWeapons])].filter(Boolean); try { if (!navOnly) { await Promise.all([ - preloadCharacterAssets(_charsToLoad), + preloadCharacterAssets(_charsToLoad, { weapons: _armasDaPartida }), preloadMapProps([...MAP_PROPS, ...((MAPS[currentMap] && MAPS[currentMap].props) || [])]), // + props do mapa (Havan: carros/estátua) - preloadAmbientLife((MAPS[currentMap] && MAPS[currentMap].ambience) || []), preloadFPArms(), // braços FP dedicados (falha → fallback procedural, sem bloquear) ]); } @@ -1134,7 +1072,7 @@ async function _startGame(team, charId, enemyFaction) { game = new Game({ renderer, textures, sfx, settings, playerCharId: charId, playerTeam: side, playerFaction: faction, enemyFaction: enemyFac, mapId: currentMap, - nickname: $('nick-input').value, testMode, mobile: TOUCH, matchRoster, + nickname: $('nick-input').value, testMode, mobile: TOUCH, matchRoster, matchWeapons, ctf: matchMode === 'ctf', // o modo agora é 100% escolha do jogador (ctfMode só define o PADRÃO ao trocar de mapa) roundsMax: matchRounds(), onMatchEnd: recordMatchStats, @@ -1142,6 +1080,19 @@ async function _startGame(team, charId, enemyFaction) { onTrainingFrames: sendTrainingFrames, }); window.__game = game; + /* Resto das armas em ocioso: o drop do chão e a troca no meio da partida precisam de malha + real, senão vira caixa procedural. Falha calada — é disponibilidade, não requisito. */ + if (!navOnly && params.get('armaslazy') !== '0') { + const meuJogo = game; + let tentativas = 0; + const espera = setInterval(() => { + if (window.__game !== meuJogo || ++tentativas > 240) { clearInterval(espera); return; } + if (meuJogo.state !== 'live') return; + clearInterval(espera); + const ocioso = window.requestIdleCallback || ((f) => setTimeout(f, 1200)); + ocioso(() => preloadWeapons().catch(() => {})); + }, 250); + } submitted = false; telemetrySent = false; // partida nova = uma linha nova de telemetria _matchEventSent = false; // partida nova = um evento rico novo (feat/telemetria) @@ -1152,10 +1103,7 @@ async function _startGame(team, charId, enemyFaction) { game.onOpenSettings = () => { game.setPaused(true); settingsReturn = 'pause-menu'; show('settings-panel'); }; // pausa nova = botão destrutivo desarmado (senão um "CLIQUE DE NOVO" velho sobrevive // até a pausa seguinte e o primeiro clique já confirmaria) - game.onPauseChange = (paused) => { - resetConfirms(); - applyCinematicScreen(paused ? 'pause-menu' : null); - }; + game.onPauseChange = () => resetConfirms(); game.onToggleSpeech = () => { settings.speech = !settings.speech; sfx.speechEnabled = settings.speech; @@ -1339,7 +1287,6 @@ function setSetupStep(step) { if (st) st.textContent = tr(matchMode === 'ctf' ? 'PASSO 1 · A PARTIDA (CTF)' : 'PASSO 1 · A PARTIDA'); if (tt) tt.textContent = tr(setupTitle); } - if (document.body.dataset.cineScreen === 'main-menu') applyCinematicScreen('main-menu'); } const openSetup = (mode, title, act) => { if (mode) { matchMode = mode; modoEscolhido = true; } // veio de SINGLE PLAYER/CAPTURE THE FLAG = escolha explícita @@ -1569,7 +1516,7 @@ function setMapMeta() { function setMapMode() { const m = $('map-mode'); if (m) { - m.textContent = matchMode === 'ctf' ? 'CAPTURE THE FLAG' : 'MATA-MATA'; + m.textContent = matchMode === 'ctf' ? tr('CAPTURE THE FLAG') : tr('MATA-MATA'); m.dataset.mode = matchMode; } const d = $('map-dots'); @@ -1602,7 +1549,6 @@ function gotoMap(i) { if (!modoEscolhido) matchMode = MAPS[currentMap].ctfMode ? 'ctf' : 'rounds'; setMapMode(); rebuildMenuBackdrop(); - loadMenuBackdrop().catch(() => {}); renderMapScreen(); // se a tela cheia estiver aberta, ela acompanha o carrossel } function stepMap(dir, ids = MAP_IDS) { @@ -1663,7 +1609,8 @@ const MAP_DATA = { upa_24h: '13/08/2026', obras_prefeitura: '13/08/2026', }; const CAT_DESC = { - TODOS: 'Mapas oficiais da casa.', + TODOS: 'O acervo inteiro, do mais jogado ao menos jogado.', + OFICIAIS: 'Mapas oficiais da casa.', ARENA: 'Combate fechado e simétrico — o duelo de angulação clássico.', FAVELA: 'Verticalidade de laje, beco e sombra: quem domina o alto dita o round.', CIDADES: 'Marcos do Brasil em escala de treta: concreto, calçada e linha reta.', @@ -1679,22 +1626,45 @@ let mapAutorFiltro = 'TODOS'; function autoresDeComunidade() { return [...new Set(MAP_IDS.filter((id) => catsDe(id).includes('COMUNIDADE')).map(autorDe))].sort(); } +/* Quantas vezes cada mapa foi escolhido, do contador que o /api/pick alimenta desde + 06/08 (picks_daily). Chega TARDE, por rede, e pode nunca chegar: a tela abre sem ele, + e quando chega redesenha. Nada aqui pode depender do número existir. */ +let mapPlays = {}; +const playsDe = (id) => mapPlays[id] || 0; +fetch('/api/map-plays') + .then((r) => (r.ok ? r.json() : null)) + .then((j) => { + if (!j || !j.plays || typeof j.plays !== 'object') return; + mapPlays = j.plays; + if (!$('map-screen')?.classList.contains('hidden')) renderMapScreen(); + }) + .catch(() => { /* sem banco/rede: a tela fica sem a estatística, e é só isso */ }); function visibleMapIds() { - /* A aba TODOS lista só os mapas OFICIAIS — os de comunidade têm a própria aba. "De comunidade" - é pela categoria (a etiqueta do crachá), que coincide com autoria não-oficial. */ - return MAP_IDS.filter((id) => mapCategory === 'TODOS' ? !catsDe(id).includes('COMUNIDADE') : catsDe(id).includes(mapCategory)); + /* TODOS = o acervo inteiro, ordenado do mais jogado pro menos (empate: ordem do catálogo, + que é estável — `sort` sem desempate deixava a lista dançar entre renders). + OFICIAIS e COMUNIDADE são recortes por categoria e mantêm a ordem do catálogo. */ + if (mapCategory === 'TODOS') { + return MAP_IDS.slice().sort((a, b) => playsDe(b) - playsDe(a) || MAP_IDS.indexOf(a) - MAP_IDS.indexOf(b)); + } + if (mapCategory === 'OFICIAIS') return MAP_IDS.filter((id) => !catsDe(id).includes('COMUNIDADE')); + return MAP_IDS.filter((id) => catsDe(id).includes(mapCategory)); } function renderMapScreen() { const img = $('ms-bg-img'); if (!img) return; /* O mapa em foco manda na aba: se não pertence à aba atual, troca pra aba que o contém. Trocas manuais de aba já re-ancoram o mapa antes, então isto não briga com elas. */ if (!visibleMapIds().includes(currentMap)) { - mapCategory = catsDe(currentMap).includes('COMUNIDADE') ? 'COMUNIDADE' : 'TODOS'; + mapCategory = catsDe(currentMap).includes('COMUNIDADE') ? 'COMUNIDADE' : 'OFICIAIS'; } const continuar = $('ms-continue')?.querySelector('span'); if (continuar) continuar.textContent = frase('continuarSetup'); img.decoding = 'async'; // decode fora da thread principal — não trava a UI da tela de mapas img.src = `/img/map-previews/${currentMap}.jpg?v=${VERSION}`; + /* O palco é o MESMO wallpaper do menu principal, não esta preview: a foto do mapa em + foco já está no card selecionado, e em tela cheia ela brigava com a grade. O `src` + acima continua sendo escrito porque é dele que a sonda de tela lê o mapa em foco. */ + const palco = document.querySelector('#map-screen .ms-bg'); + if (palco) { palco.style.setProperty('--wall', HOME_WALL); palco.style.setProperty('--wall-3x2', HOME_WALL_3X2); } $('ms-name').textContent = MAPS[currentMap].name; // separadores da ficha em verde (referência 04): texto continua o mesmo do cartaz $('ms-meta').innerHTML = ($('map-meta').textContent || '').split('·').join('·'); @@ -1709,13 +1679,23 @@ function renderMapScreen() { const desc = $('ms-cat-desc'); if (desc) desc.textContent = CAT_DESC[mapCategory] ? tr(CAT_DESC[mapCategory]) : ''; $('ms-count').textContent = `${tr('MAPA')} ${MAP_IDS.indexOf(currentMap) + 1} ${tr('DE')} ${MAP_IDS.length}`; + /* Estatística de partidas: só aparece quando o número EXISTE. Escrever "0 PARTIDAS" + num mapa que ninguém mediu é afirmar o que não se sabe — sem o contador, o crachá some. */ + const plays = $('ms-plays'); + if (plays) { + const n = playsDe(currentMap); + plays.hidden = !n; + plays.textContent = n ? `${n.toLocaleString('pt-BR')} ${tr(n === 1 ? 'PARTIDA' : 'PARTIDAS')}` : ''; + } const shown = visibleMapIds(); $('ms-strip').style.setProperty('--map-count', shown.length); $('ms-strip').innerHTML = shown.map((id) => ``).join(''); document.querySelectorAll('.ms-tab').forEach((tab) => { const on = tab.dataset.cat === mapCategory; @@ -1731,7 +1711,7 @@ function renderMapScreen() { }); requestAnimationFrame(() => $('ms-strip').querySelector('.ms-thumb.on')?.scrollIntoView({ block: 'nearest', inline: 'center' })); } -mapThumb.title = 'Ver mapa em tela cheia'; +mapThumb.title = tr('Ver mapa em tela cheia'); mapThumb.style.cursor = 'pointer'; mapThumb.onclick = () => { ui.click(); renderMapScreen(); show('map-screen'); }; $('ms-back').onclick = () => { ui.back(); show('main-menu'); }; @@ -1870,61 +1850,21 @@ const stripStep = (dir) => { }; $('strip-up').onclick = () => { ui.click(); stripStep(-1); }; $('strip-down').onclick = () => { ui.click(); stripStep(1); }; -// Cards vêm do registro único; facção sem roster aprovado aparece no -// catálogo, mas não abre o fallback procedural que o dono reprovou como Roblox. -const factionCards = [...document.querySelectorAll('.team-card[data-faction]')]; -/* BUG-42: o catálogo de pôsteres virou índice + um único hero editorial. A seleção - continua vindo dos mesmos botões/registro; esta função só projeta a linha focada no - palco, sem duplicar regra de disponibilidade nem de roster. */ -function presentFaction(card) { - if (!card || card.classList.contains('faction-excluded')) return; - const hero = $('faction-hero'); if (!hero) return; - const name = card.querySelector('.team-name')?.textContent?.trim() || '—'; - const slogan = card.querySelector('.team-slogan')?.textContent?.trim() || '—'; - const ready = card.dataset.ready === '1'; - const count = CHARACTERS.filter(c => c.team === card.dataset.faction).length; - hero.style.setProperty('--hero-art', card.style.getPropertyValue('--art')); - hero.style.setProperty('--hero-color', card.style.getPropertyValue('--tc')); - hero.style.setProperty('--hero-rgb', card.style.getPropertyValue('--tc-rgb')); - const crest = $('fh-crest'); if (crest) { crest.src = card.querySelector('.team-crest')?.src || ''; crest.alt = `Brasão ${name}`; } - $('fh-name').textContent = name; - $('fh-slogan').textContent = slogan; - $('fh-desc').textContent = card.dataset.description || ''; - $('fh-status').textContent = ready && count ? `${count} PERSONAGENS // ABRIR ELENCO` : 'ELENCO 3D EM PRODUÇÃO'; - hero.dataset.ready = ready && count ? '1' : '0'; - for (const item of factionCards) item.classList.toggle('is-preview', item === card); -} -for (const card of factionCards) { - const fac = card.dataset.faction; - const n = CHARACTERS.filter(c => c.team === fac).length; +// Contador de elenco nos cards de facção ("8 PERSONAGENS" — referência telas/02) +for (const f of ['e', 'b', 'u', 'c', 'f']) { + const n = CHARACTERS.filter(c => c.team === f.toUpperCase()).length; + const card = $('btn-team-' + f); + if (!card) continue; const chip = document.createElement('span'); chip.className = 'team-count'; chip.textContent = `${n} ${tr('PERSONAGENS')}`; - if (!n) chip.textContent = tr('INDISPONÍVEL'); card.appendChild(chip); - const ready = card.dataset.ready === '1' && n > 0; - card.setAttribute('aria-disabled', String(!ready)); - card.addEventListener('focus', () => { card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); presentFaction(card); }); - card.addEventListener('mouseenter', () => presentFaction(card)); - card.onclick = () => { - if (!ready) { ui.back(); return; } - sfx.uiClick(); pickTeam(fac); - }; -} -presentFaction(factionCards.find(card => card.dataset.ready === '1') || factionCards[0]); -if ($('fh-status')) $('fh-status').onclick = () => { - const card = factionCards.find(item => item.classList.contains('is-preview')); - if (card) card.click(); -}; -for (const [id, direction] of [['team-prev', -1], ['team-next', 1]]) { - const button = $(id); - if (!button) continue; - button.onclick = () => { - ui.click(); - const rail = document.querySelector('.team-row'); - rail?.scrollBy({ top: direction * Math.max(92, rail.clientHeight * .56), behavior: 'smooth' }); - }; } +$('btn-team-e').onclick = () => { sfx.uiClick(); pickTeam('E'); }; +$('btn-team-b').onclick = () => { sfx.uiClick(); pickTeam('B'); }; +$('btn-team-u') && ($('btn-team-u').onclick = () => { sfx.uiClick(); pickTeam('U'); }); +$('btn-team-c') && ($('btn-team-c').onclick = () => { sfx.uiClick(); pickTeam('C'); }); +$('btn-team-f') && ($('btn-team-f').onclick = () => { sfx.uiClick(); pickTeam('F'); }); $('btn-resume').onclick = () => { sfx.uiClick(); game?.resume(); }; $('btn-pause-settings').onclick = () => { sfx.uiClick(); settingsReturn = 'pause-menu'; show('settings-panel'); }; $('btn-pause-controls').onclick = () => { sfx.uiClick(); howtoReturn = 'pause-menu'; show('howto-panel'); }; @@ -2019,18 +1959,19 @@ $('char-confirm').onclick = () => { } }; -// Esconde/mostra o card da sua facção na tela de adversário; os demais continuam juntos. +// Esconde/mostra o card da sua facção na tela de adversário (btn-team-e/b/u). function setEnemyPickMode(on, myFaction) { - for (const card of factionCards) - card.classList.toggle('faction-excluded', !!(on && card.dataset.faction === myFaction)); - presentFaction(factionCards.find(card => !card.classList.contains('faction-excluded') && card.dataset.ready === '1') || - factionCards.find(card => !card.classList.contains('faction-excluded'))); + for (const f of ['e', 'b', 'u', 'c', 'f']) { + const b = $('btn-team-' + f); + if (b) b.classList.toggle('hidden', !!(on && f.toUpperCase() === myFaction)); + } } /* A MESMA tela serve dois passos e precisa DIZER qual é. Antes o único sinal era o título trocado por querySelector em 4 lugares diferentes do arquivo — e o 2º passo ficava com cara de formulário ("escolha o adversário" e três caixas iguais). Agora o passo é um estado (data-step) que a tela inteira lê: eyebrow, título, dica e o texto da barra de ação de cada placa (ver .team-cta no style.css). */ +const FACTION_NAME = { E: 'TIME E', B: 'TIME B', U: 'TRIBOS URBANAS', C: 'PALHAÇOS', F: 'FUNKEIROS' }; function setTeamStep(step, myFaction) { const ts = $('team-select'); if (ts) ts.dataset.step = step; const st = $('team-step'), tt = $('team-title'), hint = $('team-hint'); @@ -2043,7 +1984,6 @@ function setTeamStep(step, myFaction) { if (tt) tt.textContent = tr('ESCOLHA SEU LADO DA TRETA'); if (hint) hint.textContent = tr('Cada facção tem elenco, grito e jeito de brigar. Escolha o coro.'); } - if (document.body.dataset.cineScreen === 'team-select') applyCinematicScreen('team-select'); } const nickEl = $('nick-input'); @@ -2203,7 +2143,6 @@ function loadStats() { JSON.parse(localStorage.getItem(STATS_KEY) || '{}')); } async function recordMatchStats(s) { - applyCinematicScreen('match-end'); submitted = true; sendTelemetry(); // ANTES do guard de nick lá embaixo: telemetria cobre quem não registrou sendMatchEvent(s?.won ? 'won' : 'lost'); // evento rico anônimo (feat/telemetria, 016) @@ -2344,8 +2283,8 @@ let teamPreviewsDone = false; function ensureTeamPreviews() { if (teamPreviewsDone) return; teamPreviewsDone = true; - for (const fac of FACTIONS.map((f) => f.id)) { - const box = document.querySelector(`.team-card[data-faction="${fac}"] .team-chars`); + for (const [btn, fac] of [['btn-team-e', 'E'], ['btn-team-b', 'B'], ['btn-team-u', 'U'], ['btn-team-c', 'C'], ['btn-team-f', 'F']]) { + const box = document.querySelector(`#${btn} .team-chars`); if (!box) continue; const chars = CHARACTERS.filter(c => c.team === fac && GLB_CHARS.has(c.id)).slice(0, 4); if (!chars.length) continue; @@ -2374,8 +2313,10 @@ function pickTeam(faction) { currentFaction = faction; currentTeam = faction === 'B' ? 'B' : 'E'; // estado de seleção persistente nos cards: ao voltar do personagem, a tela diz qual é o SEU lado - for (const card of factionCards) - card.setAttribute('aria-pressed', String(card.dataset.faction === faction)); + for (const f of ['e', 'b', 'u', 'c', 'f']) { + const b = $('btn-team-' + f); + if (b) b.setAttribute('aria-pressed', String(f.toUpperCase() === faction)); + } const chars = CHARACTERS.filter(c => c.team === faction); // roster da facção escolhida // ?nav=1 pula o preload 3D do roster (lento) — thumbnails caem no fallback pvThumb, que // nunca dispara GLB. A transição #char-select é o que o smoke de navegação quer provar. @@ -2472,9 +2413,6 @@ function selectChar(c, row) { $('char-info-name').textContent = c.name; $('char-info-blurb').textContent = tr(c.blurb); renderCharAttrs(c); - sfxReady.then(() => { - if (selChar?.id === c.id) sfx.characterVoice(c.id, 'select', { fallbackFaction: c.team, interrupt: true }); - }); } function selectCharacterFromAvatar(c, row, roster) { @@ -2755,6 +2693,6 @@ async function openInspectionScreen(target) { if (inspectionScreen) { openInspectionScreen(inspectionScreen).catch((error) => window.__gameLaunch?.fail(error, 'screen-query')); } else if (testMode && params.get('auto')) { - const [team, char, enemyFaction] = params.get('auto').split(','); - startGame(team || 'E', char || CHARACTERS[0].id, enemyFaction || undefined); + const [team, char] = params.get('auto').split(','); + startGame(team || 'E', char || CHARACTERS[0].id); } diff --git a/public/js/version.js b/public/js/version.js index 9c12078a3..45c08809e 100644 --- a/public/js/version.js +++ b/public/js/version.js @@ -1,2 +1,2 @@ // Segue package.json e as tags v*; o manifesto acrescenta o hash do grafo JS publicado. -export const VERSION = '2.0.0-alpha.169'; +export const VERSION = '2.0.0-alpha.179'; diff --git a/public/js/weapons.js b/public/js/weapons.js index 2b591225b..a0751f680 100644 --- a/public/js/weapons.js +++ b/public/js/weapons.js @@ -11,9 +11,6 @@ export const WEAPON_IDS = ['awp', 'ak', 'm4', 'mp5', 'shotgun', 'deagle', 'pisto 'm92', 'akm', 'g3', 'revolver38', 'md97', 'carbine', 'm400', 'mosin', 'rem700', 'lmg', 'scar', 'tavor', 'famas', 'uzi', 'p90', 'svd', 'g3sg1', 'sks']; // snipers semi-auto (reusam o modelo de outra arma via MODEL_ALIAS) -// Modelos só de apresentação: não viram slot, pickup nem 27ª arma. O Bandeirante usa -// o mosquete histórico no corpo de 3ª pessoa, enquanto a balística continua no id `mosin`. -const DISPLAY_MODEL_IDS = ['mosquete']; // Snipers semi-auto novas reaproveitam a MALHA de uma arma existente (sem asset novo): // SVD←SCAR, G3SG1←G3, SKS←carabina. weaponModel/preload usam este alias. @@ -93,9 +90,6 @@ const CFG = { carbine: { len: 0.98, rot: [0, 0, 0], gripZ: 0.6, vm: 0.92 }, // natively +Z; [0,90,0] threw the barrel onto X (giant) m400: { len: 0.92, rot: [0, 270, 0], gripZ: 0.62, vm: 0.85 }, // +180: usuário confirmou invertido mosin: { len: 1.20, rot: [0, 270, 0], gripZ: 0.66, vm: 0.75 }, // +180: estava invertido - /* mosquete: o cano já nasce no eixo Z no GLB (medido; maior eixo, como nas outras) — - rot em X jogava a seção transversal no eixo da normalização. Régua: weapon-scale-check. */ - mosquete: { len: 1.45, rot: [0, 0, 0], gripZ: 0.68 }, rem700: { len: 1.15, rot: [0, 270, 0], gripZ: 0.66, vm: 0.78 }, // +180: estava invertido // arsenal-3 (military) lmg: { len: 1.10, rot: [0, 90, 0], gripZ: 0.58, vm: 0.72 }, // vm: caixão preto gigante na tela @@ -237,8 +231,10 @@ function buildMag(id) { const loadGLB = (url) => new Promise((res, rej) => loader.load(url, res, undefined, rej)); -export async function preloadWeapons() { - await Promise.all([...WEAPON_IDS, ...DISPLAY_MODEL_IDS].map(async (id) => { +/* `ids` opcional: a partida carrega SÓ as armas que ela sorteou (ver pickMatchWeapons), e o + resto chega em ocioso. Sem lista = elenco inteiro, que é o caminho do arnês e do menu. */ +export async function preloadWeapons(ids) { + await Promise.all((ids && ids.length ? ids : WEAPON_IDS).map(async (id) => { const src = MODEL_ALIAS[id] || id; // snipers reusadas carregam a malha da arma-fonte if (_cache.has(src)) return; try { const g = await loadGLB(`models/weapons/${src}.glb?v=${VERSION}`); _cache.set(src, g.scene); } @@ -326,10 +322,7 @@ export function weaponModel(id) { wrap.add(model); wrap.updateMatrixWorld(true); const box = new THREE.Box3().setFromObject(wrap); - /* Normaliza pelo MAIOR eixo, não só por Z: um `rot` errado deixa a arma torta (a régua - de orientação pega), mas nunca gigante. Régua: tools/eval/weapon-scale-check.mjs. */ - const dx = box.max.x - box.min.x, dy = box.max.y - box.min.y, dz = box.max.z - box.min.z; - const zlen = Math.max(dx, dy, dz) || 1; + const zlen = (box.max.z - box.min.z) || 1; const s = Math.min(8, Math.max(0.05, cfg.len / zlen)); // guard against a bad bbox → giant gun wrap.scale.setScalar(s); // shift so the grip point (gripZ along the barrel) sits at the origin diff --git a/public/style.css b/public/style.css index 5c087a026..bc0db13b2 100644 --- a/public/style.css +++ b/public/style.css @@ -1822,83 +1822,134 @@ html[data-ui="legacy"] #splash-enter{font-family:var(--font)} } /* ============ MAP SCREEN ============ */ -#map-screen{padding:0;justify-content:flex-start;align-items:stretch;background:#08080a; - font-family:var(--aaa-font-body)} -.ms-bg{position:absolute;inset:0;overflow:hidden;border:0;background:#101014} -.ms-bg img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block} +/* Redesign de 21/08, 3ª rodada: a tela é CABEÇALHO + ACERVO + RODAPÉ, em faixas. + O cabeçalho carrega tudo que não é lista (abas, ficha do mapa em foco, opções da + partida) e o corpo inteiro fica para a grade de mapas — que é o que a tela existe + para mostrar. O palco é o WALLPAPER do setup: a foto do mapa em foco já está no card + selecionado, e repetida em tela cheia ela brigava com a grade. */ +#map-screen{padding:0;gap:0;background:#08080a;font-family:var(--aaa-font-body); + display:grid;grid-template-rows:auto minmax(0,1fr) auto;grid-template-columns:minmax(0,1fr); + justify-content:stretch;align-items:stretch;justify-items:stretch} +/* O MESMO wallpaper do menu principal, pintado do MESMO jeito: borrão cobrindo a tela + por baixo e a arte inteira (`contain`) por cima, com a variante 3:2 na mesma faixa de + proporção. Copiar o número e não a regra era como as duas telas divergiam antes. */ +.ms-bg{position:absolute;inset:0;overflow:hidden;border:0;background:var(--bg-900);z-index:0;isolation:isolate} +.ms-bg::before,.ms-bg::after{content:"";position:absolute;pointer-events:none; + background-image:var(--wall);background-position:center;background-repeat:no-repeat} +.ms-bg::before{inset:-24px;z-index:0;background-size:cover;filter:blur(18px) brightness(.48) saturate(.72)} +.ms-bg::after{inset:0;z-index:1;background-size:contain} +@media (min-aspect-ratio:37/25) and (max-aspect-ratio:38/25){ + .ms-bg::before,.ms-bg::after{background-image:var(--wall-3x2,var(--wall))} + .ms-bg::after{background-size:cover} +} +.ms-bg img{display:none} /* o `src` serve à sonda de tela, não ao olho */ .ms-scrim{position:absolute;inset:0;pointer-events:none;z-index:1; - background:linear-gradient(90deg,rgba(8,8,10,.92) 0%,rgba(8,8,10,.7) 34%,transparent 62%),linear-gradient(0deg,rgba(8,8,10,.92) 0%,rgba(8,8,10,.5) 24%,transparent 44%),repeating-linear-gradient(0deg,transparent 0 3px,rgba(0,0,0,.05) 3px 4px)} -.ms-kicker{position:absolute;z-index:2;left:32px;top:24px; - font-family:var(--aaa-font-body);font-weight:600;font-size:12px;letter-spacing:4px; + background:linear-gradient(90deg,rgba(8,8,10,.9) 0%,rgba(8,8,10,.72) 46%,rgba(8,8,10,.66) 100%),linear-gradient(0deg,rgba(8,8,10,.92) 0%,rgba(8,8,10,.6) 30%,rgba(8,8,10,.88) 100%),repeating-linear-gradient(0deg,transparent 0 3px,rgba(0,0,0,.05) 3px 4px)} +/* ---- faixa 1: cabeçalho ---- */ +.ms-topbar{position:relative;z-index:2;padding:18px 40px 16px; + border-bottom:1px solid rgba(236,235,230,.1)} +.ms-kicker{font-family:var(--aaa-font-body);font-weight:600;font-size:12px;letter-spacing:4px; color:var(--br-faixa);text-transform:uppercase} -.ms-tabs{position:absolute;z-index:2;top:60px;left:32px;display:flex;gap:24px} -.ms-tab{padding:8px 2px;font:600 15px/1 var(--aaa-font-body);letter-spacing:3px;color:#5c5d63; +.ms-tabs{display:flex;gap:14px;flex-wrap:nowrap} +.ms-tab{padding:6px 2px;font:600 14px/1 var(--aaa-font-body);letter-spacing:3px;color:#5c5d63; background:transparent;border:0;border-bottom:2px solid transparent;cursor:pointer} .ms-tab:hover,.ms-tab:focus-visible{color:#ecebe6} .ms-tab.on{font-weight:700;color:#b4d92e;border-bottom-color:#b4d92e} -.ms-head{position:absolute;z-index:2;left:64px;top:130px;width:460px; - display:flex;flex-direction:column;align-items:flex-start;gap:14px;text-align:left} -.ms-tagrow{display:flex;align-items:center;justify-content:flex-start;gap:10px} +.ms-cat-desc{max-width:100%;margin:0;font:400 13px/1.5 var(--aaa-font-body);letter-spacing:.4px;color:#8a8b91} +.ms-author{padding:5px 12px;font:600 11px/1 var(--aaa-font-body);letter-spacing:2px;color:#5c5d63;background:rgba(16,16,20,.7);border:1px solid #2a2b31;border-radius:999px;cursor:pointer;text-transform:uppercase} +.ms-author:hover,.ms-author:focus-visible{color:#ecebe6} +.ms-author.on{color:#2e9ed8;border-color:#2e9ed8} +/* UMA linha, TRÊS colunas: navegação | ficha do mapa em foco | opções da partida. Era + uma pilha de quatro faixas empilhadas, que empurrava a grade para baixo da dobra. */ +.ms-head{display:grid;grid-template-columns:minmax(300px,340px) minmax(0,1fr) auto; + align-items:start;gap:16px 36px} +.ms-nav{min-width:0;display:flex;flex-direction:column;align-items:flex-start;gap:10px} +.ms-ficha{min-width:0;display:flex;flex-direction:column;align-items:flex-start;gap:7px;text-align:left} +.ms-tagrow{display:flex;align-items:center;justify-content:flex-start;gap:10px;flex-wrap:wrap} .ms-cat{font-size:11px;font-weight:700;letter-spacing:3px;padding:3px 10px;border:1px solid} .ms-cat[data-cat="FAVELA"]{color:#e0762a;border-color:rgba(224,118,42,.5)} .ms-cat[data-cat="ARENA"]{color:#8b8c92;border-color:rgba(139,140,146,.5)} .ms-cat[data-cat="CIDADES"]{color:#8258d8;border-color:rgba(130,88,216,.5)}.ms-cat[data-cat="COMUNIDADE"]{color:#2e9ed8;border-color:rgba(46,158,216,.5)} .ms-cat[data-cat="AI"]{color:#d8c02e;border-color:rgba(216,192,46,.5)} -.ms-cat-desc{position:absolute;z-index:2;top:100px;left:32px;max-width:520px;margin:0;font:400 13px/1.5 var(--aaa-font-body);letter-spacing:.4px;color:#8a8b91;pointer-events:none} -.ms-author{padding:5px 12px;font:600 11px/1 var(--aaa-font-body);letter-spacing:2px;color:#5c5d63;background:rgba(16,16,20,.7);border:1px solid #2a2b31;border-radius:999px;cursor:pointer;text-transform:uppercase} -.ms-author:hover,.ms-author:focus-visible{color:#ecebe6} -.ms-author.on{color:#2e9ed8;border-color:#2e9ed8} -.ms-byline{margin:10px 0 0;font:400 13px/1 var(--aaa-font-body);letter-spacing:.6px;color:#8a8b91} +/* o crachá de partidas jogadas — só existe quando o contador respondeu */ +.ms-plays{font-family:var(--font);font-weight:700;font-size:11px;letter-spacing:2px;color:#b4d92e} +.ms-plays[hidden]{display:none} +.ms-byline{margin:0;font:400 12px/1.4 var(--aaa-font-body);letter-spacing:.6px;color:#8a8b91} .ms-byline strong{color:#b9babf;font-weight:600} .ms-badge-oficial{display:inline-block;margin-left:8px;padding:2px 8px;font:700 10px/1 var(--aaa-font-body);letter-spacing:2px;color:#b4d92e;border:1px solid rgba(180,217,46,.5);border-radius:3px;text-transform:uppercase} .ms-badge-comunidade{display:inline-block;margin-left:8px;padding:2px 8px;font:700 10px/1 var(--aaa-font-body);letter-spacing:2px;color:#2e9ed8;border:1px solid rgba(46,158,216,.5);border-radius:3px;text-transform:uppercase} -.ms-count{font-family:var(--font);font-weight:600;font-size:12px;letter-spacing:2px;color:#5c5d63} -.ms-name{font-family:var(--aaa-font-display);font-weight:400;font-size:72px; - line-height:.9;letter-spacing:1px;text-transform:uppercase;color:var(--ink-100); - text-shadow:3px 3px 0 rgba(0,0,0,.5)} -.ms-meta{font-family:var(--font);font-weight:700;font-size:15px;letter-spacing:2px;color:var(--ink-100)} -.ms-meta .ms-sep{color:var(--br-faixa);margin:0 10px} -.ms-desc{font-size:16px;font-weight:400;color:#b9b9be;max-width:380px;line-height:1.5;text-shadow:var(--sh-hud)} -.ms-match-options{display:flex;align-items:flex-end;gap:10px;margin-top:2px;padding:10px 12px; - background:rgba(10,10,12,.66);border-left:2px solid var(--br-faixa);backdrop-filter:blur(8px)} -.ms-match-options label{display:flex;flex-direction:column;gap:5px;min-width:112px; +.ms-count{font-family:var(--font);font-weight:600;font-size:11px;letter-spacing:2px;color:#5c5d63} +.ms-name{margin:0;font-family:var(--aaa-font-display);font-weight:400;font-size:40px; + line-height:1;letter-spacing:1px;text-transform:uppercase;color:var(--ink-100); + text-shadow:2px 2px 0 rgba(0,0,0,.5)} +.ms-meta{margin:0;font-family:var(--font);font-weight:700;font-size:12px;letter-spacing:2.4px;color:var(--ink-100)} +.ms-meta .ms-sep{color:var(--br-faixa);margin:0 8px} +.ms-desc{margin:0;font-size:13px;font-weight:400;color:#b9b9be;max-width:62ch;line-height:1.7} +.ms-match-options{display:flex;flex-wrap:wrap;align-items:flex-end;gap:10px;margin:0;padding:11px 13px; + background:rgba(16,16,20,.8);border-left:2px solid var(--br-faixa)} +.ms-match-options label{display:flex;flex-direction:column;gap:6px;min-width:112px; font:700 10px/1 var(--aaa-font-body);letter-spacing:2px;color:#8b8c92;text-transform:uppercase} .ms-match-options select{height:32px;padding:0 28px 0 9px;border:1px solid rgba(236,235,230,.2);border-radius:0; background:#111216;color:#ecebe6;font:700 12px/1 var(--aaa-font-body);letter-spacing:1px;cursor:pointer} .ms-match-options select:focus{outline:1px solid var(--br-faixa);border-color:var(--br-faixa)} -.ms-carousel{position:absolute;z-index:2;left:64px;right:64px;bottom:96px;height:124px; - display:flex;align-items:center;gap:14px} -.ms-arrow{width:34px;height:124px;flex:0 0 34px;display:flex;align-items:center;justify-content:center; - background:rgba(13,14,17,.7);border:1px solid #26272d;color:#9a9ba1;font-size:16px;cursor:pointer} -.ms-arrow:hover,.ms-arrow:focus-visible{border-color:#b4d92e;color:#ecebe6} -.ms-viewport{flex:1;min-width:0;overflow:hidden} -.ms-strip{--map-count:1;display:grid;grid-template-columns:repeat(var(--map-count),minmax(0,196px));justify-content:center;gap:12px;width:100%;overflow:visible;scroll-behavior:smooth} +/* ---- faixa 2: o acervo, dono do corpo ---- */ +.ms-body{position:relative;z-index:2;min-height:0;display:grid;padding:18px 40px 10px} +.ms-carousel{min-height:0;display:flex;flex-direction:column;gap:8px} +.ms-arrow{flex:0 0 auto;width:100%;max-width:1180px;margin:0 auto;height:24px;display:flex;align-items:center;justify-content:center; + background:rgba(13,14,17,.7);border:1px solid #26272d;color:#9a9ba1;font-size:11px;line-height:1;cursor:pointer} +.ms-arrow:hover,.ms-arrow:focus-visible{border-color:#b4d92e;color:#ecebe6;background:rgba(13,14,17,.9)} +.ms-viewport{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;scrollbar-color:#33343b transparent} +.ms-viewport::-webkit-scrollbar{width:6px} +.ms-viewport::-webkit-scrollbar-thumb{background:#33343b} +/* a grade cresce com a tela: `auto-fill` decide o número de colunas pela largura real. + O TETO DE 3 sai da conta entre o mínimo do card e o max-width da faixa: 3×360+2×14 cabe + em 1180, 4×360+3×14 não. Mexer num dos dois números sem o outro solta uma 4ª coluna. */ +.ms-strip{--map-count:1;display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));justify-content:stretch;gap:14px;width:100%;max-width:1180px;margin:0 auto;scroll-behavior:smooth} .ms-thumb{position:relative;width:100%;min-width:0;padding:0;display:flex;flex-direction:column;cursor:pointer; - background:#101014;border:1px solid rgba(236,235,230,.12);opacity:.75; + background:rgba(16,16,20,.86);border:1px solid rgba(236,235,230,.12);opacity:.8; font-family:var(--aaa-font-body);text-align:left;text-transform:uppercase; - transition:border-color var(--t) var(--ease),filter var(--t) var(--ease)} -.ms-thumb:hover,.ms-thumb:focus-visible{filter:brightness(1.15)} + transition:border-color var(--t) var(--ease),filter var(--t) var(--ease),opacity var(--t) var(--ease)} +.ms-thumb:hover,.ms-thumb:focus-visible{filter:brightness(1.15);opacity:1} .ms-thumb.on{border:2px solid #b4d92e;opacity:1;box-shadow:0 0 26px rgba(180,217,46,.3)} -.ms-thumb-img{display:block;width:100%;height:96px;object-fit:cover} -.ms-thumb-copy{display:flex;flex-direction:column;gap:1px;padding:6px 10px 8px;background:rgba(10,10,12,.85)} +.ms-thumb-img{display:block;width:100%;height:auto;aspect-ratio:4/3;object-fit:cover} +.ms-thumb-copy{display:flex;flex-direction:column;gap:3px;padding:8px 11px 10px;background:rgba(10,10,12,.85)} .ms-thumb-name{font-weight:700;font-size:12px;letter-spacing:2px;color:#c9c9cc; white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .ms-thumb.on .ms-thumb-name{color:#b4d92e} +.ms-thumb-sub{display:flex;align-items:center;justify-content:space-between;gap:10px} .ms-thumb-cat{font-weight:600;font-size:9px;letter-spacing:2px} .ms-thumb-cat[data-cat="FAVELA"]{color:#e0762a} .ms-thumb-cat[data-cat="ARENA"]{color:#8b8c92} .ms-thumb-cat[data-cat="CIDADES"]{color:#8258d8} +/* partidas jogadas no card: número seco, sem rótulo — a coluna inteira é a mesma unidade */ +.ms-thumb-plays{font-family:var(--font);font-weight:700;font-size:10px;letter-spacing:1.5px;color:#7d8a52} +.ms-thumb.on .ms-thumb-plays{color:#b4d92e} .ms-diamond{position:absolute;top:6px;right:6px;width:8px;height:8px;background:var(--br-faixa);transform:rotate(45deg)} -.ms-dashes{position:absolute;z-index:2;left:0;right:0;bottom:74px;display:flex;justify-content:center;gap:6px} +.ms-dashes{flex:0 0 auto;display:flex;justify-content:center;gap:6px} .ms-dashes i{width:18px;height:3px;background:#33343b}.ms-dashes i.on{background:#b4d92e} -.ms-actions{position:absolute;right:64px;bottom:24px;z-index:2} -#map-screen .ms-actions .btn-big{transform:skewX(-8deg);background:linear-gradient(180deg,#cfe84a,#b4d92e);padding:14px 52px;border:0} -#map-screen .ms-actions .btn-big>*{display:block;transform:skewX(8deg);font:400 23px/1 var(--aaa-font-display);letter-spacing:3px;color:#131313;padding:0} -@media(max-height:620px){ - .ms-head{top:112px;gap:7px} - .ms-name{font-size:54px} +/* ---- faixa 3: rodapé, VOLTAR num canto e CONTINUAR no outro ---- */ +.ms-foot{position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between; + gap:18px;padding:14px 40px 22px} +.ms-actions{position:static} +#map-screen .ms-actions .btn-big{transform:skewX(-8deg);background:linear-gradient(180deg,#cfe84a,#b4d92e);padding:12px 40px;border:0} +#map-screen .ms-actions .btn-big>*{display:block;transform:skewX(8deg);font:400 20px/1 var(--aaa-font-display);letter-spacing:3px;color:#131313;padding:0} +@media(max-height:780px){ + .ms-topbar{padding:14px 32px 12px;gap:8px} + .ms-body{padding:12px 32px 8px} + .ms-name{font-size:30px} .ms-desc{display:none} - .ms-match-options{padding:7px 9px}.ms-match-options select{height:28px} + .ms-match-options{padding:9px 11px}.ms-match-options select{height:28px} + .ms-foot{padding:10px 32px 14px} +} +@media(max-width:1120px){ + /* a linha de três só sobrevive enquanto as três colunas couberem: abaixo disso as + opções descem para uma segunda linha, e a navegação segue colada na ficha. */ + .ms-head{grid-template-columns:minmax(280px,320px) minmax(0,1fr)} + #ms-match-options{grid-column:1 / -1} +} +@media(max-width:820px){ + .ms-head{grid-template-columns:minmax(0,1fr)} + .ms-strip{grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px} } /* ============ CHAR ATTRS (ficha do personagem — mockup tela 03) ============ */ @@ -1957,8 +2008,8 @@ html[data-ui="legacy"] #splash-enter{font-family:var(--font)} .ms-top-player{display:flex;align-items:center;gap:10px;font-family:var(--font);font-weight:700; font-size:var(--fs-200);letter-spacing:var(--tr-2);color:var(--ink-100);text-transform:uppercase} .ms-top-player .pp-avatar{width:34px;height:34px;font-size:12px} -#map-screen .btn-back{top:auto;left:64px;bottom:28px;padding:10px 20px; - font:600 14px/1 var(--aaa-font-body);letter-spacing:2px;color:#c9c9cc; +#map-screen .btn-back{position:static;top:auto;left:auto;bottom:auto;padding:10px 20px; + font:600 13px/1 var(--aaa-font-body);letter-spacing:2px;color:#c9c9cc; background:rgba(10,10,12,.55);border:1px solid rgba(236,235,230,.18)} /* Brasão da facção no TOPO do card (pedido do dono, 06/08) + setas do seletor de mapa @@ -1976,7 +2027,8 @@ html[data-ui="legacy"] #splash-enter{font-family:var(--font)} text-align:center;gap:2px;padding:0 10px} .team-card .team-name{display:block;text-align:center} .team-card .team-slogan{display:block;text-align:center;max-width:26ch} -.ms-arrow{width:52px;height:72px;font-size:34px;line-height:72px;background:rgba(var(--bg-900-rgb),.85); +/* setas do acervo: alvo largo no topo e no pé da coluna vertical (21/08) */ +.ms-arrow{width:100%;height:26px;font-size:12px;line-height:1;background:rgba(var(--bg-900-rgb),.85); border:1px solid var(--line-2);color:var(--am)} .ms-arrow:hover{background:var(--am);color:#141216} /* rodapé do menu: online + links de dev (substitui a frase de sátira) */ @@ -2539,9 +2591,11 @@ body[data-cine-screen] #cine-chrome{opacity:1} .weapon-amount{display:none} /* ---------- AJUSTES DO CRÍTICO (14/08) ---------- */ -/* O cabeçalho do mapa segue a caixa de 460px da referência 04. */ -.ms-head{width:460px} -.ms-name{font-size:72px} +/* A ficha do mapa mora no CABEÇALHO desde 21/08 e ocupa a faixa inteira: largura vem do + flex, não de uma caixa fixa de 460px, e o nome encolheu porque divide a linha com as + opções da partida. */ +.ms-head{width:auto;max-width:none} +.ms-name{font-size:40px} /* #3 — valor numérico ao lado da barra de atributo */ .attr{grid-template-columns:92px 1fr auto} .attr b{font-family:var(--font);font-weight:700;font-size:var(--fs-100);color:var(--ink-100);text-align:right} diff --git a/scripts/ci/autofix_allowlist.py b/scripts/ci/autofix_allowlist.py new file mode 100644 index 000000000..812c79212 --- /dev/null +++ b/scripts/ci/autofix_allowlist.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""A trava do autofix: decide se o que o bot mexeu pode virar commit. + +O bot só pode tocar em ARQUIVO GERADO — o que uma ferramenta do repositório +reescreve inteiro a partir do código. Se o conserto encostar em qualquer outro +caminho, o run aborta e comenta em vez de commitar. + +A regra não é conveniência: sem ela, o primeiro conserto errado reescreve o mapa +de um colaborador e ninguém repara. Com ela, o pior caso do bot é regenerar uma +doc que já era derivada. + +Uso: git status --porcelain | python3 scripts/ci/autofix_allowlist.py + python3 scripts/ci/autofix_allowlist.py --selftest +""" +import sys + +# Caminhos que uma ferramenta REGERA por inteiro. Crescer esta lista é decisão de +# dono: cada entrada nova é um arquivo que o bot passa a poder sobrescrever sozinho. +PERMITIDOS = ( + "ARCH.generated.md", + "README.md", + "STATUS.md", + "SCRIPTS.md", + "docs/", + "tools/eval/ARCH.md", +) + +# Caminhos que NUNCA entram, mesmo que um prefixo permitido pareça cobri-los. O +# `.github/` fica de fora porque um bot que edita o próprio workflow que o governa +# consegue ampliar a própria permissão em um commit. +PROIBIDOS = ( + ".github/", + "scripts/", + "supabase/", + "package.json", + "package-lock.json", + "vercel.json", +) + + +def permitido(caminho: str) -> bool: + if any(caminho.startswith(p) for p in PROIBIDOS): + return False + return any(caminho == p or caminho.startswith(p) for p in PERMITIDOS) + + +def separa(caminhos: list[str]) -> tuple[list[str], list[str]]: + ok = [c for c in caminhos if permitido(c)] + fora = [c for c in caminhos if not permitido(c)] + return ok, fora + + +def caminhos_do_porcelain(texto: str) -> list[str]: + """`git status --porcelain` -> lista de caminhos. + + Renomeio vem como `R velho -> novo`; o que importa é o destino, senão um + rename para fora da lista passaria como se fosse o arquivo de origem. + """ + saida = [] + for linha in texto.splitlines(): + if len(linha) < 4: + continue + caminho = linha[3:].strip() + if " -> " in caminho: + caminho = caminho.split(" -> ", 1)[1] + saida.append(caminho.strip('"')) + return saida + + +def selftest() -> int: + casos = [ + ("doc gerada passa", ["ARCH.generated.md"], True), + ("pasta docs passa", ["docs/docs/comecando.md"], True), + ("código do jogo NÃO passa", ["public/js/main.js"], False), + ("mapa de colaborador NÃO passa", ["public/js/map_havan.js"], False), + ("workflow NÃO passa", [".github/workflows/ci.yml"], False), + ("script de CI NÃO passa", ["scripts/ci/pr_classify.py"], False), + ("package.json NÃO passa", ["package.json"], False), + ("uma proibida contamina o lote", ["STATUS.md", "public/js/game.js"], False), + ("nada mexido é lote vazio", [], True), + ("CHANGELOG não é gerado por ferramenta nossa", ["CHANGELOG.md"], False), + ("lote típico de release só toca gerado", ["README.md", "STATUS.md", "docs/docs/comecando.md"], True), + ] + erros = 0 + for nome, caminhos, esperado in casos: + _, fora = separa(caminhos) + obtido = not fora + ok = obtido == esperado + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} {nome}: fora={fora!r}") + + porcelain = ' M ARCH.generated.md\nR docs/a.md -> public/js/game.js\n?? novo.txt\n' + lidos = caminhos_do_porcelain(porcelain) + ok = lidos == ["ARCH.generated.md", "public/js/game.js", "novo.txt"] + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} renomeio é julgado pelo DESTINO: {lidos!r}") + + print("selftest verde" if not erros else f"{erros} caso(s) vermelho(s)") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + # `--caminhos`: a entrada já é uma lista crua (o `git diff --name-only` do merge), + # e não o porcelain. É o modo que o resolvedor de conflito usa. + if "--caminhos" in sys.argv: + caminhos = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] + else: + caminhos = caminhos_do_porcelain(sys.stdin.read()) + ok, fora = separa(caminhos) + if fora: + print("BLOQUEADO: o conserto encostou em arquivo que o bot não pode reescrever:") + for c in fora: + print(f" - {c}") + return 1 + for c in ok: + print(c) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/ensure_labels.py b/scripts/ci/ensure_labels.py index 6810e02e0..e54053b22 100644 --- a/scripts/ci/ensure_labels.py +++ b/scripts/ci/ensure_labels.py @@ -17,18 +17,50 @@ "crash-auto": ("B60205", "crash de produção reportado por /api/jserror ou prod-watch"), "stale-backlog": ("c2e0c6", "Issue antiga ou desalinhada com o checkout atual; precisa revalidação"), "needs-repro": ("f9d0c4", "Issue precisa passos de reprodução mais concretos"), + "preview-autorizado": ("0e8a16", "Mantenedor revisou o SHA atual e liberou preview do fork na Vercel"), + "pronto-pra-merge": ("0e8a16", "Portões verdes e diff dentro do aceito: falta só o merge, que é humano"), } -def main() -> int: - for label in sys.argv[1:]: +def comandos(nomes: list[str]) -> list[list[str]]: + """Os `gh label create` que ESTE pedido produz. + + Nome fora do dicionário some em silêncio, e foi assim que `preview-autorizado` + passou meses sendo pedida por um workflow sem nunca ser criada — o job de preview + de fork inteiro virou código morto atrás de um rótulo que ninguém podia aplicar. + """ + out = [] + for label in nomes: if label not in LABELS: continue color, description = LABELS[label] - subprocess.run( - ["gh", "label", "create", label, "--color", color, "--description", description, "--force"], - check=True, - ) + out.append(["gh", "label", "create", label, "--color", color, "--description", description, "--force"]) + return out + + +def selftest() -> int: + casos = [ + ("rótulo conhecido vira um comando", ["safe-automerge"], ["safe-automerge"]), + ("desconhecido é ignorado", ["nao-existe"], []), + ("conhecido junto de desconhecido não contamina", ["nao-existe", "bot-fixable"], ["bot-fixable"]), + ("lista vazia não faz nada", [], []), + ("preview-autorizado está registrado", ["preview-autorizado"], ["preview-autorizado"]), + ] + erros = 0 + for nome, pedido, esperado in casos: + obtido = [c[3] for c in comandos(pedido)] + ok = obtido == esperado + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} {nome}: {obtido!r}") + print("selftest verde" if not erros else f"{erros} caso(s) vermelho(s)") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + for comando in comandos(sys.argv[1:]): + subprocess.run(comando, check=True) return 0 diff --git a/scripts/ci/workflow_security_check.py b/scripts/ci/workflow_security_check.py index 1740ca9f9..53baec243 100644 --- a/scripts/ci/workflow_security_check.py +++ b/scripts/ci/workflow_security_check.py @@ -9,6 +9,16 @@ def preview_failures(source: str) -> list[str]: + """Contrato do preview de fork POR APROVAÇÃO (desenho antigo). + + Vale enquanto o `preview-bot.yml` tiver o job que publica: ele roda `vercel build`, + isto é, executa o build DO FORK com o token no ambiente, e por isso depende de um + mantenedor revisar o SHA. Quando o preview passou a ser build-sem-segredo + + deploy-sem-código (ver `separacao_failures`), este job deixou de ser necessário — + mas enquanto existir, ele tem de manter TODAS as travas. + """ + if 'vercel deploy' not in source: + return [] # o job de publicação saiu do arquivo: nada a guardar aqui errors = [] required = { 'types: [opened, synchronize, reopened, labeled]': 'evento labeled ausente', @@ -27,13 +37,84 @@ def preview_failures(source: str) -> list[str]: for marker, message in required.items(): if marker not in source: errors.append(message) - if '--add-label "preview-autorizado"' in source: + if '--add-label' in source and 'preview-autorizado' in source: errors.append('workflow autoaprova código de fork') if 'preview_autorizado=true' in source: errors.append('workflow decide autorização sem mantenedor') return errors +def separacao_failures(build: str, deploy: str) -> list[str]: + """Contrato do preview de fork POR SEPARAÇÃO (desenho de 22/08). + + A aprovação manual existia porque UM job fazia as duas coisas: rodava o build do + fork E tinha o token. Separando, os dois lados ficam seguros sozinhos e ninguém + precisa clicar: + + PRV1 quem COMPILA roda código do fork e não recebe segredo — `pull_request` (não + `pull_request_target`), e nenhuma referência a `secrets.` no arquivo; + PRV2 quem PUBLICA roda no contexto base (`workflow_run`), que é o que lhe dá o + segredo mesmo vindo de fork; + PRV4 quem PUBLICA não interpola `${{ }}` dentro de `run:`. A expressão é substituída + no TEXTO do script antes do shell existir, então valor com aspas ou `$(...)` vira + comando — e no caminho de deploy os valores vêm de fora (o número do PR atravessa + um artefato escrito por job que rodou código do fork, a URL é saída de comando). + O CodeQL pegou exatamente isso aqui, como injeção crítica, antes do merge. + PRV3 quem PUBLICA não executa NADA do PR: sem checkout da branch do fork, sem + `npm ci`, sem `npm run`, e o deploy é `--prebuilt` (só envia arquivo). + Furar isto devolve o token para as mãos de quem abriu o PR. + """ + errors = [] + if not build or not deploy: + return ['preview por separação incompleto: falta preview-build.yml ou preview-deploy.yml'] + + # Só a INSTRUÇÃO conta. Os dois arquivos explicam em comentário o que NÃO fazem + # ("não roda npm ci", "não usa pull_request_target") e ler o comentário como se + # fosse código acusaria justamente quem documentou a trava. + def codigo(texto: str) -> str: + return '\n'.join(l for l in texto.splitlines() if not l.lstrip().startswith('#')) + + build, deploy = codigo(build), codigo(deploy) + + if 'pull_request_target' in build: + errors.append('PRV1 o job que COMPILA usa pull_request_target — passaria a enxergar segredo rodando código do fork') + if 'pull_request:' not in build: + errors.append('PRV1 o job que COMPILA não roda em pull_request') + if 'secrets.' in build: + errors.append('PRV1 o job que COMPILA referencia `secrets.` — ele roda código do fork e não pode ter o que roubar') + + if 'workflow_run:' not in deploy: + errors.append('PRV2 o job que PUBLICA não roda em workflow_run — sem contexto base não há segredo em PR de fork') + if 'secrets.VERCEL_TOKEN' not in deploy: + errors.append('PRV2 o job que PUBLICA não usa o token da Vercel') + + # PRV4: `${{ }}` só pode aparecer em `env:`/`with:`/`if:`, nunca dentro do script. + dentro_de_run = False + for linha in deploy.splitlines(): + despido = linha.strip() + if re.match(r'run:\s*\|', despido): + dentro_de_run = True + continue + if dentro_de_run: + # o bloco acaba quando a indentação volta para o nível da chave do passo + if despido and not linha.startswith(' '): + dentro_de_run = False + elif '${{' in linha: + errors.append(f'PRV4 `{despido[:60]}` interpola expressão dentro de run: — passe por env:') + + if '--prebuilt' not in deploy: + errors.append('PRV3 o deploy não é --prebuilt: estaria construindo, e construir é executar código do PR') + for proibido, motivo in ( + ('actions/checkout', 'faz checkout — traria código do PR para o job que tem o token'), + ('npm ci', 'roda npm ci — executaria script do PR'), + ('npm run', 'roda npm run — executaria script do PR'), + ('vercel build', 'roda vercel build — é o build do fork com o token no ambiente'), + ): + if proibido in deploy: + errors.append(f'PRV3 o job que PUBLICA {motivo}') + return errors + + def supply_failures(workflows: dict[Path, str]) -> list[str]: errors = [] for path, source in workflows.items(): @@ -54,7 +135,26 @@ def read_workflows(root: Path = Path('.github/workflows')) -> dict[Path, str]: } +BUILD = Path('.github/workflows/preview-build.yml') +DEPLOY = Path('.github/workflows/preview-deploy.yml') + + +def _ler(p: Path) -> str: + return p.read_text(encoding='utf-8') if p.exists() else '' + + def selftest(source: str) -> list[str]: + build, deploy = _ler(BUILD), _ler(DEPLOY) + separacao = { + 'compila-com-segredo': (build + '\n env:\n X: ${{ secrets.VERCEL_TOKEN }}\n', deploy), + 'compila-com-target': (build.replace(' pull_request:', ' pull_request_target:'), deploy), + 'publica-sem-run': (build, deploy.replace(' workflow_run:', ' schedule:')), + 'publica-faz-checkout': (build, deploy + '\n - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683\n'), + 'publica-constroi': (build, deploy.replace('--prebuilt', '')), + 'publica-interpola': (build, deploy.replace('gh pr comment "$PR_NUM"', 'gh pr comment "${{ steps.pr.outputs.numero }}"')), + } + missed_sep = [n for n, (b, d) in separacao.items() if not separacao_failures(b, d)] + mutations = { 'auto-label': source + '\n# --add-label "preview-autorizado"\n', 'sem-ator': source.replace('repos/$REPO/collaborators/$ACTOR/permission', 'repos/$REPO'), @@ -65,10 +165,22 @@ def selftest(source: str) -> list[str]: 'action-mutável': re.sub(r'actions/checkout@[0-9a-f]{40}', 'actions/checkout@v4', source), 'cli-mutável': re.sub(r'vercel@\d+\.\d+\.\d+', 'vercel@latest', source), } + # As mutações acima descrevem o contrato ANTIGO (preview por aprovação). Quando o + # job que publicava sai do arquivo, elas deixam de ter alvo — testá-las ali seria + # exigir mordida de uma régua que não tem mais o que guardar. O contrato novo é + # medido pelas mutações de `separacao` logo acima. + antigo_vivo = 'vercel deploy' in source missed = [ name for name, mutated in mutations.items() - if not (preview_failures(mutated) + supply_failures({WORKFLOW: mutated})) + if antigo_vivo and not (preview_failures(mutated) + supply_failures({WORKFLOW: mutated})) ] + # Fornecimento (action e CLI presas) vale sempre, e o alvo passou a ser os dois + # arquivos novos: um tem as actions, o outro tem a CLI da Vercel. + fornecimento = { + 'action-mutável': (BUILD, re.sub(r'actions/checkout@[0-9a-f]{40}', 'actions/checkout@v4', build)), + 'cli-mutável': (DEPLOY, re.sub(r'vercel@\d+\.\d+\.\d+', 'vercel@latest', deploy)), + } + missed += [n for n, (alvo, mutado) in fornecimento.items() if not supply_failures({alvo: mutado})] with tempfile.TemporaryDirectory() as tmp: mutant = Path(tmp) / 'mutable-action.yaml' mutant.write_text('steps:\\n - uses: actions/checkout@v4\\n', encoding='utf-8') @@ -83,12 +195,14 @@ def main() -> int: args = parser.parse_args() source = WORKFLOW.read_text(encoding='utf-8') workflows = read_workflows() - errors = preview_failures(source) + supply_failures(workflows) + errors = (preview_failures(source) + + separacao_failures(_ler(BUILD), _ler(DEPLOY)) + + supply_failures(workflows)) if errors: for error in errors: print(f'WFS FAIL: {error}') return 1 - print('WFS PASS: preview de fork exige aprovação manual presa ao SHA') + print('WFS PASS: quem compila código de fork não tem segredo; quem tem segredo não executa código de fork') if args.selftest: missed = selftest(source) if missed: diff --git a/scripts/fetch-audio.sh b/scripts/fetch-audio.sh index 58d27b672..3dd9e6b25 100755 --- a/scripts/fetch-audio.sh +++ b/scripts/fetch-audio.sh @@ -18,7 +18,7 @@ if [ -f "$DEST/manifest.json" ]; then fi mkdir -p "$DEST" echo "Baixando pacote de áudio de: $URL" -curl -fsSL "$URL" -o /tmp/csbrasil-audio.zip +curl --retry 5 --retry-delay 3 --retry-all-errors --connect-timeout 20 -fsSL "$URL" -o /tmp/csbrasil-audio.zip unzip -o -q /tmp/csbrasil-audio.zip -d "$DEST/" [ -f "$DEST/manifest.json" ] || cp "$DEST/manifest.example.json" "$DEST/manifest.json" echo "Pronto. Áudio instalado em $DEST/." diff --git a/scripts/fetch-decals.sh b/scripts/fetch-decals.sh index b9697f17b..dcafbcf85 100755 --- a/scripts/fetch-decals.sh +++ b/scripts/fetch-decals.sh @@ -24,6 +24,6 @@ if [ -d "$DEST" ] && [ "$(ls -1 "$DEST"/*.png 2>/dev/null | grep -cv '/or-')" -g fi mkdir -p "$DEST" echo "Baixando decalques de: $URL" -curl -fsSL "$URL" -o /tmp/csbrasil-decals.zip +curl --retry 5 --retry-delay 3 --retry-all-errors --connect-timeout 20 -fsSL "$URL" -o /tmp/csbrasil-decals.zip unzip -o -q /tmp/csbrasil-decals.zip -d "$DEST/" echo "Pronto. $(ls -1 "$DEST"/*.png | wc -l | tr -d ' ') decalques em $DEST/." diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index b7d933366..a60f4026f 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -569,7 +569,7 @@ body>footer a:hover{color:var(--amber)}
- +
{/* MARCA: logo + canarinho, que é o pedido do dono e é personagem do elenco. diff --git a/src/pages/api/map-plays.ts b/src/pages/api/map-plays.ts new file mode 100644 index 000000000..238e611ac --- /dev/null +++ b/src/pages/api/map-plays.ts @@ -0,0 +1,45 @@ +// GET /api/map-plays - soma por mapa do contador que o /api/pick alimenta (picks_daily, +// kind='mapa'). Colunas em PORTUGUÊS e RLS fechada: só service_role alcança. +import type { APIRoute } from 'astro'; +import { supabaseAdmin } from '../../lib/supabase'; +import { rateLimit } from '../../lib/ratelimit'; + +export const prerender = false; + +const JANELA_DIAS = 365; // "total" na prática: a tabela nasceu em 06/08/2026 + +const resposta = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { + 'content-type': 'application/json', + // o número muda devagar e a tela abre muito: quase todo o tráfego morre na borda + 'cache-control': 'public, max-age=0, s-maxage=300, stale-while-revalidate=900', + }, + }); + +export const GET: APIRoute = async ({ request }) => { + // a tela de mapas ordena por este número: falha aqui NUNCA pode derrubar a escolha + if (!supabaseAdmin) return resposta({ plays: {} }); + + const ip = request.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown'; + if (!(await rateLimit(supabaseAdmin, 'map-plays', ip, 60, 60))) return resposta({ plays: {} }, 429); + + const desde = new Date(Date.now() - JANELA_DIAS * 864e5).toISOString().slice(0, 10); + const { data, error } = await supabaseAdmin + .from('picks_daily') + .select('key, n') + .eq('kind', 'mapa') + .gte('dia', desde); + if (error) { + console.error('[api/map-plays]', { error: error.message }); + return resposta({ plays: {} }); + } + + const plays: Record = {}; + for (const linha of (data ?? []) as Array<{ key: string; n: number }>) { + if (typeof linha?.key !== 'string') continue; + plays[linha.key] = (plays[linha.key] || 0) + (Number(linha.n) || 0); + } + return resposta({ plays }); +}; diff --git a/src/pages/index.astro b/src/pages/index.astro index 63345252e..0ee01ece4 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,6 +1,5 @@ --- import pkg from '../../package.json'; -import { FACTIONS } from '../../public/js/factions.js'; import { DISCORD_URL, TELEGRAM_URL, GITHUB_URL, SUPPORT_URL_BR, SUPPORT_URL_INTL } from '../lib/site'; /* ESTÁTICA de propósito: o Stateloop não publica SSR e `prerender = false` aqui @@ -579,16 +578,6 @@ window.__GEO_LANG__ = (function () { - - -