Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
Coleção de jogos casuais online desenvolvidos nos GameStudios do MarquitosPT compilados numa Arcada.
Translation pending
GamesJavaScript · CSSGitHub @MarquitosPTOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Coleção de jogos casuais online desenvolvidos nos GameStudios do MarquitosPT compilados numa Arcada. Blazor Web App (.NET 10, render mode Interactive Server) com ASP.NET Core Identity e SQLite, em src/MarquitosArcade/. Substituiu o antigo servidor Node/Express + scores.json que o site usava originalmente — o site é multi-página (não uma SPA) e o iOS Safari reavalia o modo standalone da PWA a cada navegação de página completa; a "enhanced navigation" do Blazor evita esse full page reload. A mudança também abriu caminho para login de amigos e leaderboard persistido em BD.Read the full README ↗
Coleção de jogos casuais online desenvolvidos nos GameStudios do MarquitosPT compilados numa Arcada.
Em linha: arcade.marquitos.pt
Blazor Web App (.NET 10, render mode Interactive Server) com ASP.NET Core Identity e SQLite, em src/MarquitosArcade/. Substituiu o antigo servidor Node/Express + scores.json que o site usava originalmente — o site é multi-página (não uma SPA) e o iOS Safari reavalia o modo standalone da PWA a cada navegação de página completa; a "enhanced navigation" do Blazor evita esse full page reload. A mudança também abriu caminho para login de amigos e leaderboard persistido em BD.
src/MarquitosArcade/Components/Pages/Home.razor, wwwroot/styles.css: portal principal com branding e catálogo de jogos. O catálogo é gerado a partir do array ArcadeCatalog.Games (Components/ArcadeCatalog.cs, partilhado com a página /acerca) — cada jogo é um cartão com a sua capa, o título em overlay e a sua cor (classes .theme-*). styles.css é a folha de estilos global do site — cobre o portal, a página de pontuações e as páginas de conta (/Account/...); cada jogo tem as suas próprias folhas de estilo, em games/<slug>/css/. Ver Tema.src/MarquitosArcade/wwwroot/theme.js: escolha do tema claro/escuro (ver Tema).src/MarquitosArcade/wwwroot/covers/: capas 16:9 dos jogos (WebP) usadas no catálogo — são screenshots reais de cada jogo, gerados por tools/covers/ (ver Capas dos jogos).src/MarquitosArcade/Components/Pages/Pontuacoes.razor: página dedicada às pontuações em /pontuacoes, com um painel por jogo (array Games no @code). Lê os tops diretamente da base de dados no servidor, via ScoresEndpoints.GetTopScoresAsync — o mesmo método que serve o endpoint GET /api/scores/:gameId, mas sem passar por HTTP.src/MarquitosArcade/Components/Pages/Acerca.razor: página /acerca com a ficha da arcada (título, versão, autor, data de publicação e copyright) e a lista dos jogos com a versão e a data de cada um. Os dados vêm do Components/ArcadeCatalog.cs; a versão do site é o <Version> do .csproj. Tem o link na nota do fim da home, a seguir ao da privacidade.src/MarquitosArcade/Components/Pages/Privacidade.razor e Suporte.razor: as duas páginas de texto — política de privacidade em /privacidade e suporte em /suporte. São os URLs apontados pela app na App Store (política de privacidade e suporte), por isso são para manter estáveis. Chega-se lá pela nota no fim da home, a seguir ao catálogo (.home-meta em Home.razor) — e não pelo rodapé, que é uma barra de uma linha onde os links partiam a linha em ecrãs estreitos. O texto vive no próprio .razor (não há CMS nem markdown por trás), no painel de vidro das páginas simples (.page-shell), com a tipografia longa na secção .longform do styles.css. Duas notas ao mexer nelas: a política tem duas datas a atualizar (a do topo e a da linha de versão no fim), e o suporte diz que a arcada não envia emails — se algum dia houver servidor de email configurado, a resposta sobre a palavra-passe esquecida deixa de ser verdade.src/MarquitosArcade/wwwroot/games/<slug>/: um jogo por pasta, cada um com o seu index.html (só markup), css/, js/ (módulos ES) e assets/. Ver Estrutura de um jogo e, para o porquê desta organização em vez de um projeto .NET por jogo, docs/estrutura-dos-jogos.md.
tasca-do-ze/: mini-jogo "Tasca do Zé" (gestão de pedidos), com leaderboard persistido via /api/scores/tasca-do-ze.pong/: Pong Retro, com modo 1 jogador (vs. CPU, pontuação submetida via /api/scores/pong) e 2 jogadores.maze-run/: Maze Run, labirintos por níveis — apanhar os cristais abre a saída, e há guardas a impedi-lo. Pelo caminho há cristais de gelo que os congelam, portais que ligam duas pontas do labirinto e portas trancadas com a sua chave (ver As peças do Maze Run). Os níveis vão-se desbloqueando à medida que se concluem, e o progresso fica guardado na conta de quem tem sessão iniciada (ver Progresso e níveis). Os labirintos não estão desenhados à mão: saem de uma semente por nível (buildMaze em js/maze.js), como as pistas do Pixel Racing saem do buildTrack — acrescentar um nível é acrescentar uma entrada ao array LEVELS do js/levels.js, e mais nada.terras-do-reino/: Terras do Reino, economia medieval contínua num tabuleiro isométrico com relevo — semear e colher trigo, cortar madeira, tirar pedra, pescar nos lagos, abrir minas de ouro, moinhos, padarias, vacarias, pocilgas e oficinas, lã e tecelagem, vinhas, destilaria e taberna, algodão, alfaiataria, teatro, hotel para os visitantes, escola, centro de saúde e posto de correios, galinheiro, cana-de-açúcar, arroz, pastelaria e joalharia, e comerciar com três vilas vizinhas geridas pelo CPU, que crescem sozinhas e são as rivais na tabela da prosperidade. Não há guerra nem fim de partida: o reino fica gravado (na conta, para quem tem sessão iniciada) e, fora do jogo, fica em pausa: ao voltar, continua do ponto exato onde ficou. Ver .src/MarquitosArcade/wwwroot/lib/arcade/: SDK partilhado pelos jogos (áudio, leaderboard, armazenamento, viewport do canvas, ciclo de jogo, barra de topo, ecrã de arranque). Módulos ES sem dependências externas. O splash.css/splash.js/splash-boot.js são a exceção que também serve o portal — ver Ecrã de arranque.tools/games/smoke-test.mjs: smoke-test dos jogos em Chromium headless, corrido em cada pull request por .github/workflows/jogos-smoke-test.yml. Ver Testar os jogos..github/workflows/build-dotnet.yml: compila o site (dotnet build) em cada pull request — o smoke-test dos jogos não passa pelo compilador. Ver Compilar o site num pull request.src/MarquitosArcade/Scores/ScoresEndpoints.cs: endpoint genérico GET/POST /api/scores/:gameId, persistido na tabela Scores (EF Core + SQLite). Se o pedido vier de um utilizador autenticado, o nome do leaderboard vem da conta (evita spoofing de nomes); caso contrário aceita o nome livre submetido pelo jogo.src/MarquitosArcade/Progress/ProgressEndpoints.cs: endpoint genérico GET/PUT /api/progress/:gameId, para os jogos com níveis. Guarda um JSON opaco por (conta, jogo) na tabela GameProgress. Ver Progresso e níveis.src/MarquitosArcade/Scores/ScoreMaintenance.cs: limpeza da tabela Scores no arranque — apaga as pontuações que ficaram a apontar para contas já eliminadas. Ver Pontuações.src/MarquitosArcade/Web/CachePolicy.cs: política de cache HTTP — immutable para os URLs com impressão digital (?v=), no-cache para todo o resto. Ver Cache do browser.src/MarquitosArcade/Data/: ApplicationDbContext, ApplicationUser e as migrations do EF Core.src/MarquitosArcade/Components/Account/: páginas de login/registo/gestão de conta scaffolded pelo template Identity do ASP.NET Core (login em /Account/Login, registo em /Account/Register, gestão em /Account/Manage). Sem confirmação por email — não há servidor de email configurado, por isso ficaria a bloquear amigos convidados. O cookie de sessão (configurado em Program.cs, ConfigureApplicationCookie) é sempre persistente e dura um ano — isto é uma arcada de amigos instalada como PWA, não um banco, e uma sessão que expirava ao fechar a app já causou pontuações publicadas como "Anónimo": o jogo continuava a mostrar o nome guardado no localStorage de uma visita anterior (ver O nome do jogador) sem o jogador reparar que a conta, essa, já não tinha sessão.O site usa um tema "glass": superfícies de vidro fosco (backdrop-filter) sobre um fundo animado de auroras de néon, em duas variantes — clara e escura.
O estado do tema vive no atributo data-theme do <html>:
data-theme | significado |
|---|---|
| (sem atributo) | segue o sistema (prefers-color-scheme) |
light | claro, por escolha explícita do utilizador |
dark | escuro, por escolha explícita do utilizador |
O botão na barra de topo (MainLayout.razor) cicla entre os três estados. A lógica está em wwwroot/theme.js, carregado no <head> antes do primeiro paint para não haver "flash" do tema errado, e guarda a escolha em localStorage. O ícone visível do botão é escolhido por CSS a partir do data-theme — nada de JS a repintar DOM, por isso sobrevive à enhanced navigation do Blazor (que troca o <body> sem recarregar a página) sem o site precisar de render mode interativo.
Em styles.css as cores são todas tokens CSS (--bg, --glass-bg, --accent, ...) definidos três vezes: no :root (claro, a base), no @media (prefers-color-scheme: dark) restringido a :root:not([data-theme="light"]), e em :root[data-theme="dark"]. Nenhuma cor pode ter a sua única definição dentro do media query, senão a escolha explícita do utilizador deixa de ganhar. Para acrescentar uma cor nova, acrescenta-a nos três sítios.
Os jogos têm folhas de estilo próprias, mas seguem a mesma linguagem. O Pixel
Racing é o exemplo: os ecrãs são painéis de vidro por cima da pista — o
backdrop-filter desfoca o canvas que está a desenhar por trás, por isso o menu
mostra a pista escolhida, viva, em vez de um fundo pintado. Os tokens estão em
games/pixel-racing/css/theme.css e ali só existe a variante escura (o mundo do
jogo é escuro em todas as pistas, e vidro claro por cima dele não teria
contraste). O HUD, esse, é desenhado no canvas, onde não há backdrop-filter:
o vidro é imitado à mão com fundo translúcido, contorno de 1px e um risco de luz
no topo (glassPanel em js/render.js).
A arcada e cada jogo abrem com o logótipo em ecrã inteiro, "A carregar..." e uma
barra a crescer, sobre um fundo de raios e pixel art (wwwroot/lib/arcade/splash.css,
splash.js e splash-boot.js). O markup vive na página que o mostra — em
Components/App.razor no portal, no index.html de cada jogo —, porque tem de
estar pintado no primeiro frame, antes de correr JavaScript nenhum.
Fica no ar 2 segundos, mesmo quando já está tudo pronto — contando já com a
pausa na barra cheia e o fade de saída. É de propósito: dá à
arcada um arranque de consola em vez de um salto seco para o menu. O splash.js
gere três relógios para isso não se virar contra o jogador:
| Relógio | O que faz |
|---|---|
minDuration | O chão, do arranque até sair. 2s por omissão; muda-se com data-splash-min. |
ready() | O conteúdo por baixo está montado. Sem ele a barra pára nos 92%. |
maxDuration | O teto. Aos 12s desiste de esperar e sai na mesma. |
Quem chama ready() é o main.js de cada jogo (window.__arcadeSplash?.ready(),
no mesmo espírito do window.__arcadeTheme do tema); se ninguém chamar, o load
da página serve de sinal. E o CSS ainda tem um último travão — uma animação que
esconde o ecrã aos 15s — para o caso de o próprio splash-boot.js não chegar a
correr: um módulo em falta nunca pode deixar o site tapado para sempre. É também
por isso que o splash-boot.js é carregado pelo seu próprio <script> e não vem
pela cadeia de imports do jogo: se o main.js rebentar a carregar, o ecrã de
arranque sai na mesma e vê-se o erro em vez de uma barra eterna.
Duas diferenças entre o portal e os jogos:
data-splash-once="session"). Sem
isto, ir de / para /pontuacoes custava 2 segundos de cada vez. Os jogos não
levam o atributo, por isso arrancam sempre que se abre um.reapply(). Pela mesma razão que o tema (ver acima), a
enhanced navigation ressincroniza o <body> com o HTML do servidor — e esse
HTML traz sempre o ecrã de arranque. O data-permanent no elemento e o
reapply() no enhancedload garantem que ele não volta do fundo do baú.O fundo não é uma imagem: são gradientes e sprites SVG embutidos no CSS, para não
custar nenhum pedido extra logo no arranque. Só o logótipo (logo3.png) é um
ficheiro, e vai com <link rel="preload"> em todas as páginas. É isso que também
lhe permite servir retrato e paisagem sem cortes — nada tem tamanho fixo, e em
@media (orientation: portrait) a pixel art sai do meio e vai para as quatro
pontas, para não cair por cima do logótipo num ecrã estreito.
Ao contrário do resto do site, este ecrã é sempre escuro: a arte da arcada é
escura e trocar de tema a meio do arranque dava um flash. Os tokens de cor dele
vivem só no splash.css.
cd src/MarquitosArcade
dotnet run
As migrações do EF Core aplicam-se automaticamente no arranque (Database.Migrate() em Program.cs), incluindo a criação do Data/app.db na primeira execução.
A arcada usa um único processo ASP.NET Core para todos os jogos, em vez de um serviço por jogo. Razões:
marquitos-arcade) — ter vários serviços implicaria vários Web Apps ou um proxy reverso à frente deles, complexidade desnecessária para jogos casuais de baixo tráfego./api/scores/:gameId é genérico e serve qualquer jogo novo sem duplicar código de servidor.Se algum jogo precisar de lógica de servidor muito diferente (ex. websockets para multiplayer em tempo real), faz sentido isolá-lo num serviço próprio nessa altura — não antes.
Cada jogo é uma pasta com os seus ficheiros — não um projeto .NET. O porquê dessa escolha (e quando é que vale a pena mudar) está em docs/estrutura-dos-jogos.md.
wwwroot/games/<slug>/
index.html markup apenas — sem <style> nem <script> inline
css/ uma folha por zona do ecrã (base, hud, screens, ...)
js/
main.js ponto de entrada, carregado com <script type="module">
config.js constantes de afinação, sem lógica
state.js estado mutável da partida
... um módulo por área (física, desenho, controlos, áudio)
assets/ PNG, SVG e sons deste jogo
Regras que mantêm isto arrumado:
index.html é só markup. Zero CSS e zero JavaScript inline.config.js, não espalhados pelo código.wwwroot/lib/arcade/.Os jogos importam o que têm em comum de wwwroot/lib/arcade/, por caminho absoluto:
import { createViewport, createLoop } from '/lib/arcade/index.js';
import { createScoreClient } from '/lib/arcade/scores.js';
| Módulo | O que resolve |
|---|---|
audio.js | Ciclo de vida do AudioContext e bips sintetizados |
music.js | Motor da música de fundo: partitura, reverb, agendador, botão Música (ver Música de fundo) |
scores.js | Cliente de /api/scores/:gameId, cache offline, nome do jogador e da conta |
progress.js | Cliente de /api/progress/:gameId: níveis desbloqueados e marcas, na conta e no aparelho |
storage.js | localStorage que não rebenta em Safari privado |
viewport.js | Canvas em ecrã inteiro, nítido em Retina e por baixo do notch |
loop.js | Ciclo requestAnimationFrame com delta-time limitado |
dom.js | Seletores, escapeHtml, grupos de ecrãs e de botões |
topbar.js | Barra de topo comum (arcada, acerca, pausa, sair) |
about.js | Janela "Acerca": título, versão, autores, publicação e copyright (about.css) |
splash.js | Ecrã de arranque: tempo mínimo, barra de progresso e saída |
math.js | clamp, lerp, ângulos, aleatórios, shuffle |
Os três jogos resolvem o nome no mesmo sítio — bindPlayerNameInput, no
scores.js do SDK —, para o quadro de pontuações não ficar com três regras
diferentes. A ordem, da que manda para a que cede:
Tu, Anónimo, Cozinheiro(a) Anónimo).Duas regras que não são óbvias e que já custaram um bug:
forBoard(), não current(). Quem não deu nome envia vazio
e é o servidor que decide: o nome da conta, ou "Anónimo". Assim os três jogos
mostram a mesma coisa na tabela, em vez de "Tu", "Anónimo" e
"Cozinheiro(a) Anónimo" conforme o jogo de origem. O current() continua a
servir o ecrã — na pista o carro é mesmo "Tu".O servidor guarda o nome que o jogo enviar, seja quem for o jogador: quem tem de garantir que uma pontuação de conta fica com o nome da conta é o cliente.
A música dos jogos é sintetizada nota a nota no browser, sem ficheiros de áudio.
O motor é comum (lib/arcade/music.js); cada jogo traz, no seu js/music.js, só
a partitura e os instrumentos. O motor dá:
bar('D5 3, E5 1, F5 2, - 2') (nota e duração em
colcheias, - para pausa, # e b nas alterações) devolve as notas de um
compasso na grelha de semicolcheias; midi('F#5') e freq(m) para o resto.bus.route(node, time, { pan, send }), bus.noise(time, stopAt) e bus.filter(type, f, q).setTimeout acorda de 30 em 30 ms
e marca com precisão de amostra as notas dos 150 ms seguintes, pelo relógio do
AudioContext. O jogo só diz o que toca em cada semicolcheia.setWanted) e já há AudioContext.setBpm(bpm) acelera ou abranda a música; a tocar,
muda no início do compasso seguinte (a meio soa a tropeção). Para o resto do
que o jogo quer ouvir — mais ou menos instrumentos — o music.js do jogo lê o
seu próprio estado em cada passo (ver o Maze Run).// js/music.js
import { bar, createMusic } from '/lib/arcade/music.js';
import { audio } from './audio.js'; // o mesmo AudioContext dos efeitos
const BARS = ['C5 4, E5 4', 'G5 8'].map(bar);
export const music = createMusic({
audio, storageKey: 'oMeuJogoMusic_v1', bpm: 90, bars: BARS.length,
// opcional: volume, fadeIn/fadeOut, compressor, reverb, e o
// setup(bus) para os nós fixos dos instrumentos (fica no bus)
onStep(barIndex, s, time, bus) {
for (const note of BARS[barIndex]) if (note.step === s) flauta(bus, time, note);
}
});
// js/main.js
import { bindMusicButton, unlockAudioOnGesture } from '/lib/arcade/music.js';
const syncMusic = () => music.setWanted(!game.paused && document.visibilityState === 'visible');
bindMusicButton(els.musicBtn, music, { resume: resumeAudio, onToggle: syncMusic });
unlockAudioOnGesture(resumeAudio, syncMusic); // começa após o ecrã de arranque, se o browser deixar; senão, no primeiro toque ou tecla
O botão #musicBtn da barra de topo (a nota, com o traço .musicSlash quando
está desligada) e as três regras de CSS que o acompanham são iguais nos
Gulosinhos, no Terras do Reino, na Memória Animal, no Pixel Racing e no Maze Run; na Tasca do Zé o mesmo
desenho vai num dos botões redondos do cabeçalho, ao lado do 🔊, que cala tudo,
música incluída.
O compasso não tem de ser de 4/4: o passo da grelha é sempre a semicolcheia, e
stepsPerBar: 12 dá compassos de três tempos (o vira da Tasca do Zé).
Para ouvir a música sem abrir o jogo — depois de mexer na partitura ou na
mistura, ou para a mostrar a alguém — tools/games/render-music.mjs grava cada
uma em WAV (num OfflineAudioContext, com o mesmo código do jogo) e diz o pico
e o RMS, para comparar volumes:
cd tools/games
node render-music.mjs # todos → tools/games/music-out/
node render-music.mjs --only pixel-racing --loops 2
node render-music.mjs --seed 1 --out /tmp/depois
A gravação é de uma volta tal como soa no menu. music.schedule(ctx, { at, fromBar, bars }) grava só alguns compassos, e dá para gravar em pedaços,
mudando entre eles o que o jogo pede (foi assim que se ouviram os humores do
Maze Run).
Com --seed o Math.random fica determinístico e duas gravações do mesmo
código saem iguais: é assim que se confirma que um refactor não mudou o som.
tools/games/smoke-test.mjs abre cada jogo num Chromium headless, joga-o durante
alguns segundos e falha se houver erro de JavaScript, módulo ou folha de estilos
que não carregue, ecrã inicial em branco, ou canvas que não chegue a desenhar.
Serve os ficheiros com um servidor estático próprio, por isso não é preciso ter
o ASP.NET a correr nem o SDK do .NET instalado. Os pedidos a /api/* respondem
503 de propósito — os jogos têm de aguentar o servidor em baixo, e isso fica assim
coberto pelo teste.
cd tools/games
npm install
npx playwright install chromium # só na primeira vez
node smoke-test.mjs # todos os jogos
node smoke-test.mjs --only pong # só um
Corre em cada pull request pelo workflow .github/workflows/jogos-smoke-test.yml
— o workflow de deploy só arranca depois do merge em main, por isso sem isto
nada verificava uma mudança antes de entrar. Se falhar, os screenshots de cada
jogo ficam anexados à execução como artefacto, para se ver em que ecrã o jogo se
perdeu.
Para refactors, o par --out/--compare compara os ecrãs antes e depois. O ecrã
de menu é determinístico e tem de bater certo ao pixel; os fotogramas de jogo
variam com o relógio e são informativos:
git worktree add /tmp/antes HEAD
node smoke-test.mjs --root /tmp/antes/src/MarquitosArcade/wwwroot --out /tmp/ref
node smoke-test.mjs --out /tmp/novo --compare /tmp/ref
O smoke-test acima não compila nada — serve o wwwroot estaticamente. Quem trata
disso é o workflow .github/workflows/build-dotnet.yml, que corre
dotnet build --configuration Release em cada pull request. É de propósito o
mesmo comando do passo "Build with dotnet" do workflow de deploy: se passa no
pull request, o deploy compila.
Sem ele, um erro de C# ou de Razor só aparecia depois do merge em main, na
única execução que também publica. Em main este workflow não corre — quem
compila lá é o próprio workflow de deploy, e não vale a pena pagar o build duas
vezes.
src/MarquitosArcade/wwwroot/games/<slug>/ com a estrutura acima. O pong/ é o mais pequeno dos três e serve bem de modelo.
Copiar de lá também o bloco #arcadeSplash do index.html (trocando o nome do jogo em .arcade-splash-caption), os dois <link> e o <script> do splash-boot.js no <head>, e o window.__arcadeSplash?.ready() no fim do main.js — ver Ecrã de arranque.createScoreClient('<slug>') do SDK, que fala com GET/POST /api/scores/<slug>.
Se tiver níveis a desbloquear, usar também createProgressClient('<slug>'), que fala com GET/PUT /api/progress/<slug> — ver Progresso e níveis.ABOUT no config.js (título, versão, data de publicação) e pôr o botão #aboutBtn a seguir ao #arcadeLink, com o <link> do /lib/arcade/about.css — o createAboutDialog(ABOUT, { opener }) do SDK trata da janela. Um jogo com ecrãs sobrepostos próprios pode, em vez disso, pôr a ficha num deles com renderAboutDetails(ABOUT) (como a Tasca do Zé).GAMES em tools/games/smoke-test.mjs, com um guião que o jogue durante alguns segundos.GAMES em tools/covers/capture-covers.mjs e correr o script (ver Capas dos jogos).Games em Components/ArcadeCatalog.cs — o catálogo mostra as novidades primeiro e os jogos mais antigos vão descendo para o fim — (slug, título, tagline, descrição, emoji, tema, capa, e a versão e a data de publicação iguais às do ABOUT do jogo) e, se o tema for novo, uma classe .theme-<jogo> em styles.css com a cor (--game-accent) e o fundo da capa (). Só isso: a tipografia dos cartões é do catálogo e é igual para todos (ver ).Os cartões da home estão numa grelha, e numa grelha o cartão mais alto de uma linha estica os outros. Por isso duas coisas estão fixas:
--game-accent): pinta o risco por cima do título, o subtítulo e o halo ao
passar o rato.line-clamp: 3 em .game-card-body p, com um
min-height de três linhas). Assim os cartões têm todos a mesma altura em
qualquer largura, e quem escreve uma descrição nova não tem de contar
caracteres — só de saber que o que passar das três linhas não se lê. Na
prática, as que lá estão andam pelos 90 caracteres.O risco da cor tem de ficar acima do topo do título: o bottom do
.game-marquee::before é menor do que o padding-top do .game-marquee. Com
os dois iguais, o risco assenta em cima das letras.
Cada cartão do catálogo mostra um screenshot real do jogo — capturado a jogar, não um mockup — com o título em overlay num banner, na cor do próprio jogo. As imagens vivem em wwwroot/covers/<slug>.webp (16:9, 960x540).
Para as regerar (por exemplo, depois de mudar o aspeto de um jogo):
cd tools/covers
npm install # playwright
npx playwright install chromium
# noutro terminal: cd src/MarquitosArcade && dotnet run
node capture-covers.mjs --base http://localhost:5000
node capture-covers.mjs --base http://localhost:5000 --only pong # só um jogo
O script abre cada jogo num Chromium headless, joga-o com o guião definido no array GAMES, recorta a zona interessante em 16:9 e grava o WebP. Como os jogos têm elementos aleatórios (pedidos, ângulo da bola, posição dos carros), cada execução dá um fotograma diferente — vale a pena espreitar o resultado antes de fazer commit.
Jogos que ainda não existem não têm screenshot. Enquanto o Maze Run esteve por lançar, o cartão dele usou um labirinto desenhado à mão em SVG; agora que o jogo existe, a capa é um fotograma dele como a dos outros, e o placeholder (e o script que o gerava) saíram do repositório.
Não há dashboard na página principal — as pontuações vivem todas em
Components/Pages/Pontuacoes.razor, que gera um painel por jogo a partir do
array Games no @code da página.
Também não há botão de pontuações nos jogos: o ecrã inicial de cada um só tem "Arcade" (voltar ao portal) e "Acerca". Quem quiser ver as pontuações vê-as na arcada. Os jogos continuam a enviar as pontuações no fim de cada partida.
A página é renderizada no servidor e lê os tops diretamente da base de dados com
ScoresEndpoints.GetTopScoresAsync. É o mesmo método por trás de
GET /api/scores/:gameId, mas chamado sem passar por HTTP: os jogos é que usam
o endpoint, a partir do browser.
O parâmetro ?jogo=<slug> destaca o painel desse jogo — é o que o botão 🏆 da
barra de topo dos jogos usa, junto com a âncora #<slug>.
Cada linha diz também se a pontuação foi feita com sessão iniciada: as entradas
sem conta (UserId a null, o campo registered do ScoreDto) levam um
(não registado) em letra mais pequena a seguir ao nome (.player-tag no
styles.css). Sem conta o nome não está reservado, por isso a marca evita que
uma pontuação anónima passe por ser a de um jogador registado com o mesmo nome.
Quando alguém elimina a conta em /Account/Manage/DeletePersonalData, as
pontuações dessa conta saem da tabela Scores na mesma transação que remove o
utilizador — é o que a política de privacidade promete, e é por isso que não
ficam entradas órfãs com o nome de quem já saiu.
Isso só passou a acontecer depois de o site estar no ar, por isso o arranque
apaga também as órfãs antigas (UserId a apontar para uma conta que já não
existe em AspNetUsers), a seguir ao Database.Migrate() — ver
Scores/ScoreMaintenance.cs. Não há chave estrangeira com ON DELETE CASCADE
entre as duas tabelas porque acrescentá-la obrigaria o SQLite a reconstruir a
tabela Scores e a migration rebentaria logo nestas mesmas linhas órfãs.
O Maze Run é o primeiro jogo da arcada com níveis que se vão desbloqueando, e isso obrigou a uma pergunta que as pontuações não fazem: onde é que fica guardado o que já se abriu? A resposta são dois sítios, e é de propósito.
| onde | quem | o que resolve |
|---|---|---|
localStorage | toda a gente | jogar sem conta, sem rede e sem esperar pelo servidor |
tabela GameProgress | quem tem sessão iniciada | recomeçar em qualquer nível já aberto, noutro aparelho |
O cliente é o wwwroot/lib/arcade/progress.js, partilhado — o Maze Run é o
primeiro jogo a usá-lo, mas nada nele é do Maze Run. Ele grava sempre no
aparelho primeiro e manda para o servidor a seguir, com um atraso curto para
várias mudanças seguidas irem num só pedido (e uma última gravação quando a
página se esconde, para fechar o separador a meio de um nível não custar o
nível). Sem sessão iniciada, o servidor responde stored: false e o jogo
continua a jogar-se na mesma — não é um erro, é um jogador sem conta.
O servidor não sabe o que lá está dentro. O GET/PUT /api/progress/:gameId
guarda um JSON opaco por (conta, jogo), com um teto de 8 kB, exatamente como o
jogo o escreveu. É a mesma escolha do endpoint das pontuações: um jogo novo com
níveis não obriga a mexer no servidor nem a criar uma migration. O que é do jogo
— a forma do objeto e a regra de junção — vive no games/maze-run/js/progress.js.
A regra de junção é a parte que não é óbvia. As duas cópias podem discordar (jogou-se sem conta e iniciou-se sessão depois; jogou-se no telemóvel e no computador), e quem ganha é o melhor dos dois, campo a campo: o nível mais alto aberto, mais pontos, melhor tempo, mais estrelas. Nunca a cópia "mais recente" — a mais recente pode ser a de um aparelho onde se jogou menos, e ninguém quer perder níveis por ter aberto o jogo no sítio errado.
O progresso guardado tem versão, e um nível novo entra no meio dos que já
havia. As marcas estão guardadas pelo número do nível, por isso um progresso
gravado com outra numeração não diz a verdade sobre esta — o nível 3 de então
pode ser o 5 de agora. Em vez de o mostrar errado, sobe-se o VERSION em
games/maze-run/js/progress.js e recomeça-se; quem faz a recusa é o accept
do cliente do SDK, por onde passa tudo o que se lê, do aparelho e da conta.
Isto serve enquanto o jogo não estiver no ar: a partir daí, a marca de cada
nível tem de passar a ficar guardada por um nome próprio do nível, que não muda
quando ele muda de sítio na lista.
Duas notas que já custariam um bug:
totalScore). Guardado, ficava a discordar de si próprio à
primeira junção de duas cópias.js/level.js, e não no ecrã de resultados: um ecrã que não chegue a montar-se
nunca pode ser a razão de se perder o que se acabou de fazer.Eliminar a conta apaga também o progresso, na mesma transação que remove o
utilizador e as pontuações (ver DeletePersonalData.razor) — é o que a política
de privacidade promete. Ao contrário da tabela Scores, esta nasceu já com essa
limpeza feita, por isso não há órfãs antigas para varrer no arranque.
Os níveis do Maze Run não estão numa lista a rolar: estão em páginas, com setas, arrasto e as setas do teclado. Uma lista a rolar num telemóvel esconde o que vem a seguir atrás do próprio dedo, e num jogo de níveis o que interessa é ver de uma vez o que já se abriu e o que falta.
Quantos cabem numa página decide-o o CSS, não o JavaScript. As variáveis
--page-cols e --page-rows (em css/carousel.css) mudam com o media query, e
o js/carousel.js lê-as para repartir os cartões:
| onde | por página |
|---|---|
| telemóvel ao alto | 2 × 2 = 4 |
| telemóvel ao comprido | 3 × 1 = 3 |
| tablet e computador | 3 × 2 = 6 |
Assim a regra de quantos cabem vive num sítio só — quem sabe o tamanho do ecrã é o CSS. Ao rodar o aparelho o número muda, e o carrossel refaz as páginas mantendo à vista o cartão que lá estava.
Os cartões têm todos a mesma altura, e isso não é acaso: cada um tem sempre as mesmas cinco linhas, pela mesma ordem e todas presentes mesmo quando não há nada para pôr numa delas.
| linha | o que diz |
|---|---|
| número | Nível 7 |
| nome | uma palavra, que nunca quebra em duas linhas |
| peças | portais, portas e gelo que o nível tem (vazia nos primeiros) |
| estado | Conclui o nível 6, Por jogar ou a melhor marca |
| estrelas | as três, apagadas no que ainda não se fez |
Daí os nomes dos níveis serem de uma palavra (LEVELS em js/levels.js):
num cartão estreito, um nome que quebre rouba uma linha e desalinha a fila
toda. O .levelName ainda leva nowrap com reticências como rede, mas é rede
— o nome deve caber. As filas usam grid-auto-rows: 1fr em vez da altura do
conteúdo, senão a caixa do labirinto, que tem proporção fixa, arredondava para
um píxel diferente de fila para fila.
As setas mudam de sítio conforme o que falta no ecrã. Ao alto, num ecrã estreito, descem para o fundo encostadas à direita, com os pontos das páginas à esquerda: nos lados, cada seta roubava uns 40px de largura, quase um terço de um cartão. Ao comprido é ao contrário — largura é o que sobra e altura o que falta —, por isso voltam aos lados e a linha dos pontos desaparece.
Três armadilhas que este ecrã ensinou:
touch-action: pan-y, ver css/screens.css) para o arrasto não levar o
documento atrás, e essa trava alcançaria também um scroll nativo lá dentro: o
touch-action efetivo é a interseção do elemento com o dos seus antepassados.swallowClick em js/carousel.js).display: none.E uma que não é do carrossel mas apareceu com ele: o browser aumenta sozinho o
corpo de texto de um bloco comprido quando a página fica larga (o font
boosting, pensado para artigos lidos ao telemóvel). Ao rodar o telemóvel, a
nota do fim deste ecrã passava a letra graúda enquanto tudo à volta ficava
igual. Quem o desliga é o text-size-adjust: 100% no css/base.css.
São quarenta e oito níveis, e a curva sobe de quatro em quatro: cada degrau apresenta ou aperta uma coisa — o tamanho do labirinto, mais um guarda, uma peça nova — e os quatro níveis do degrau dão tempo para a aprender antes do seguinte. Do 1 ao 4 há só cristais e dois guardas; o gelo entra no 5, os portais no 9, a porta trancada no 17 e a segunda porta no 25.
Quarenta e oito é também o número que enche as páginas do carrossel em todos os formatos: 12 páginas de 4 ao alto, 16 de 3 ao comprido, 8 de 6 no computador.
As sementes dos níveis não foram escolhidas à mão. Nem todos os labirintos
dão para trancar (ver a secção a seguir), e uma receita pode sair com uma peça a
menos sem ninguém dar por isso. Cada semente foi procurada — monta-se o nível,
verifica-se que cumpre a receita toda, e tenta-se a seguinte até dar. O cenário
maze-run-mecanicas do smoke-test faz a mesma verificação aos quarenta e oito a
cada execução, por isso uma receita que deixe de cumprir falha o teste em vez de
chegar a quem joga.
Além dos cristais e dos guardas, um nível pode ter três coisas. Todas se ligam
pela receita (js/levels.js) e nenhuma precisa de um mapa desenhado à mão:
| peça | o que faz | campo na receita |
|---|---|---|
| Cristal de gelo | congela os guardas uns segundos; congelados não andam nem apanham | freezers: 2 |
| Portal | liga duas pontas do labirinto; o jogador e os guardas usam-no | portals: 1 |
| Porta trancada | corta o caminho até a sua chave aparecer; também trava os guardas | doors: 1 |
Três decisões que não são óbvias, e o porquê:
FREEZE_SECONDS). Somar fazia com que guardar
cristais valesse mais do que jogar bem, e o nível passava a ganhar-se com o
inventário em vez de com o caminho. E não sobrevive a uma vida perdida — era
prémio a mais por um erro.distanceField) trata um portal como uma porta ao lado — sem isso,
um guarda passava ao lado do portal sem o ver e o jogador tinha um atalho que
a perseguição não conhecia.enterPortal em js/walker.js). Se essa célula for a única ligação ao que
está do outro lado dela, fecha ali o labirinto — só lá se entra caindo do
outro portal, e um cristal ou a saída lá dentro fazem um nível que parece
impossível. Por isso placePortals só põe uma ponta onde, com todas as
pontas fechadas ao mesmo tempo, se continua a chegar a pé do início a todo o
labirinto. Pelo mesmo motivo duas pontas nunca ficam encostadas uma à outra —
quem saísse de uma caía logo na outra e era atirado outra vez. O smoke-test
verifica as duas coisas nos quarenta e oito níveis.Uma porta só é uma porta se trancar mesmo alguma coisa. A forma ingénua de a
colocar — escolher uma célula no caminho para a saída — não funciona: os
labirintos são "entrançados" de propósito (o braid abre becos sem saída, para
uma perseguição ter sempre uma volta a dar), e num labirinto com laços fechar
uma célula ao calhar quase nunca corta o caminho. Dá-se a volta, e a porta passa
a enfeite.
O que placeDoors faz:
Nem todos os labirintos dão para isto. Num nível com portas, a semente
escolhe-se pelo que sai: monta-se, conta-se o que lá ficou e troca-se a
semente até a receita ser cumprida. O cenário maze-run-mecanicas do
smoke-test monta os níveis todos e falha se alguma receita der um nível com
peças a menos, uma porta que não tranque a saída, uma chave que não se consiga
alcançar ou uma peça fora do alcance — é a rede que deixa acrescentar níveis sem
medo.
As paredes de um nível são pintadas uma vez para um canvas à parte e dali
copiadas a cada frame (ver paintMaze em js/render.js). Uma porta que abre
não pode obrigar a repintar tudo, por isso uma porta fechada não é parede:
a grelha do labirinto (maze.grid) é a forma escavada e nunca muda, e as portas
fechadas vivem num conjunto à parte (maze.blocked).
Daí haver duas perguntas diferentes a fazer ao labirinto, e confundi-las dar bugs difíceis de ver:
| pergunta | quem responde | quem usa |
|---|---|---|
| "isto é parede?" | isWallStatic | o desenho das paredes e os cartões do menu |
| "dá para passar aqui?" | isFloor/isWall | o andar, as saídas de uma célula, os guardas |
Como o mapa dos guardas se refaz a cada frame, uma porta que abre entra nele sozinha — não há nada em cache para invalidar.
Uma perseguição contra o relógio, ao piano com bateria (js/music.js,
no motor comum). Ré menor a 126 BPM, 20 compassos em loop
(A, B e uma pausa de timbalões que acaba num rufo), com o Lá maior e o seu Dó
sustenido a fechar cada frase, que é o acorde que deixa tudo em suspenso. A mão
esquerda do piano é um ostinato em semicolcheias que nunca pára — o relógio
a andar — e a direita uma melodia curta e sincopada. O bombo cai no 1, no "e"
do 1 e no 3, como passos a correr.
A música segue o jogo. A cada frame, o main.js diz-lhe o que se passa
(setMusicMood), e ela muda na semicolcheia seguinte:
| humor | quando | o que se ouve |
|---|---|---|
menu | menus, contagem, resultados | ostinato, melodia, baixo longo e um prato por tempo |
run | a correr no labirinto | entra a bateria toda e o baixo em colcheias |
hurry | os últimos 10 s (HURRY_FROM) | acelera para 142 BPM, pratos e baixo em semicolcheias, notas fantasma na tarola |
frozen | guardas congelados pelo cristal de gelo | bateria e baixo calam-se; um sino de vidro dobra a melodia duas oitavas acima |
hold | apanhado por um guarda, fim do nível | só o ostinato |
Cala-se (com um fade) na pausa e com a página escondida, e começa no primeiro
toque ou tecla. O botão Música da barra de topo liga-a e desliga-a
(mazeRunMusic_v1); os efeitos, incluindo o "tic" dos últimos segundos, ficam
por cima dela.
Versão 1.0.0, lançada a 26 de setembro de 2026. Esteve em testes desde 24 de setembro, fora do catálogo. As pontuações desse período já estavam a ser guardadas e aparecem no quadro desde o lançamento.
Um jogo novo pode passar pela mesma fase: com
IsPlayable: falseemComponents/ArcadeCatalog.cs, aparece no catálogo como "Em breve", sem link, mas joga-se indo direto a/games/<slug>/. Numa app afixada, onde não há barra de endereço, o cartão tem uma entrada secreta: toque duplo com três dedos (no computador, Alt + duplo clique) abre o jogo (wwwroot/secret-play.js). Para esconder também o painel das pontuações, comenta-se a linha do jogo emPontuacoes.razor. Para lançar, desfazem-se esses pontos.
O primeiro jogo da arcada sem partidas: um reino que cresce enquanto se joga e continua de onde ficou. Inspirado no tabuleiro de peças do Carcassonne — o mapa é um tabuleiro de casas em perspetiva isométrica, com colinas que sobem, lagos que descem e a "placa" de terra à vista nas bordas —, mas é um jogo de economia, não de conquista: ninguém ataca ninguém.
IDLE_TAX_SHARE) — senão encher o mapa de casas era a melhor jogada.js/boats.js), rema
pelo lago, pesca à cana — de vez em quando um peixe salta na ponta da linha
— e volta ao cais; como a gente nas estradas, os barcos só se veem. O mapa
garante sempre um lago com margem livre no território inicial.
No nível 3 do castelo abrem a lã e o vinho: curral de ovelhas
(trigo → lã) → centro de tecelagem (lã → rolos de tecido), e a vinha
(uma cultura, como o trigo: semeia-se, cresce e vindima-se — à mão ou pelo
celeiro) → destilaria (uvas → vinho) → taberna. No nível 4, o
campo de algodão (outra cultura) e a alfaiataria (algodão + rolos de
tecido → fatos e vestidos, os bens mais caros das feiras), o teatro e a
hotel (lodges em BUILDINGS): ao fim de cada dia recebe até 12
visitantes — tantos quanto o contentamento do povo atrair —, que jantam do
que sobrar na despensa depois do povo e pagam 12 moedas de estadia cada um.
Com um hotel aberto, parte da gente nas estradas são visitantes, de
chapéu e mala na mão, a chegar ao hotel ou a sair dele.
Também no nível 4 abrem os serviços do reino (service em BUILDINGS):
cada um, com gente a trabalhar, serve até 80 moradores e dá o seu efeito na
proporção do povo servido (derived.boost, calculado em updateServices) —
a escola sobe os impostos até +20%, o centro de saúde põe as
oficinas a trabalhar até 15% mais depressa e o posto de correios
melhora os preços do mercado até 10% (vende-se mais caro e compra-se mais
barato). Nas estradas veem-se as crianças a caminho da escola, mais
pequenas e de mochila, e os carteiros de farda vermelha ( em
).
No , o (cultura) → (cana →
açúcar), o (cultura que tem de ficar à beira de água; o arroz é
comida), a (ovos + farinha + açúcar + leite → bolos, a
comida mais cara) e a (ouro → joias, que valem uma fortuna nas
feiras e que a joalharia vende ao povo, ver abaixo). As culturas são as
entradas com em .Os objetivos (js/quests.js) são o tutorial: uma lista por ordem que leva
de uma casa e um campo até ao castelo no nível máximo, cada um com recompensa.
Depois dela, o jogo continua com marcos de prosperidade que vão dobrando.
Três vilas (TOWNS em js/config.js), cada uma com o seu ofício: o que
produzem fica barato no mercado, o que procuram fica caro. Crescem sozinhas,
edifício a edifício, e comerciam entre si em caravanas que se veem a atravessar
o mapa. A tabela 👑 Reinos compara a prosperidade de todos.
O preço de cada bem sai do "stock" que as vilas têm dele: no ponto de referência é o preço base, com pouco sobe, com muito desce. Vender enche o stock, por isso despejar cem trigos de uma vez rende menos do que vender aos poucos; com o tempo, o stock volta ao equilíbrio que as vilas ditam. De tempos a tempos há uma feira numa vila, que faz de um bem o mais procurado durante dois dias.
anchorFor em js/iso.js). Os desenhos das peças continuam feitos
para o bloco de 2x2 (js/draw.js); as árvores e os rochedos, de uma casa só,
carimbam-se mais pequenos.js/world.js, ruído de valor em js/rng.js).
O relevo e as florestas leem o ruído a meia resolução — as colinas e os lagos
têm o tamanho de antes, com margens mais finas.
À volta do castelo garante-se o que o começo precisa — árvores, rochas, água e
uma veia de ouro ao alcance do castelo no nível 3 —, e não só em contagem: tem de
haver uma casa livre com árvores (ou rochas) à volta, onde caibam o lenhador e a
pedreira. As vilas escolhem entre
vários sítios o que tem mais terra à volta. O smoke-test verifica isto em 200
sementes.js/draw.js, js/sprites.js). A parte estática de cada peça é pintada uma
vez numa cache à escala do ecrã (js/sprite-cache.js); a parte viva — pás do
moinho, fumo, vacas, bandeiras, brilho do ouro — desenha-se a cada frame.js/render.js): o chão de cada casa e logo a seguir o que está em cima dela.
É o que faz uma colina tapar o que está atrás. O chão (relva, lados,
estradas) é a exceção: são milhares de casas, por isso pinta-se por blocos
de 11x11 numa imagem à parte, à escala do ecrã, e só se repinta quando muda o
que o bloco tem ou o zoom; os blocos compõem-se por baixo das peças.camera.rot em js/iso.js): cada casa do mapa
passa para uma "grelha da vista", rodada à volta do centro do mapa, e só essa
é projetada; o resto do jogo continua a falar em casas do mapa. A ordem do
pintor, os lados das colinas, os blocos do chão e o toque (pickTile) são
os da grelha da vista. As peças também rodam: as primitivas de js/draw.js
pintam as faces viradas para quem olha, as portas e janelas das faces de
costas não se veem, e as peças de várias partes pintam-nas por ordem de
profundidade (layered). A luz é a do ecrã, vem sempre da esquerda. Ficam
sempre de frente a mina (um monte de rocha com a entrada à vista) e as pás
do moinho (que se vira para o vento).O reino grava-se pelo mesmo cliente do SDK que o Maze Run usa
(lib/arcade/progress.js): no aparelho sempre, na conta com sessão iniciada.
Como o servidor aceita até 8 kB por jogo, a gravação leva a semente do mapa em
vez do mapa, e os edifícios como listas curtas de números (js/save.js); as
vilas levam só quantos edifícios têm, porque crescem sempre para os mesmos
sítios. As colinas aplanadas vão como a lista dos índices das casas (fl),
aplicada ao mapa antes de se porem os edifícios, e as estradas como um bit por
casa do quadrado de 60x60 à volta do castelo, em base64 (r, 600 caracteres
fixos). A gravação é a v2 (mapa de 88x88); uma v1, do tempo em que cada casa
era um edifício, passa a v2 ao carregar, dobrando as coordenadas — o relevo é
amostrado de modo a que o reino caia no mesmo sítio, e o chão debaixo de cada
edifício é limpo à força, para nunca ficar nenhum em cima de um lago. Um reino com setenta edifícios ocupa perto de 1,5 kB.
A regra de junção é diferente da do Maze Run: ganha a cópia com mais tempo de jogo. Um reino não se junta campo a campo como as marcas de um nível — são dois mundos diferentes —, e o que tem mais horas é o que mais custaria perder. Pela mesma razão, o botão do menu só fica ativo depois de a conta responder: fundar um reino novo antes disso podia pôr um reino de cinco minutos por cima de um de cinco horas guardado noutro aparelho.
Fora do jogo o reino fica em pausa. Ao esconder a página a meio do jogo
(outro separador, outra aplicação, o ecrã do telemóvel a apagar), o jogo grava e
abre o ecrã de pausa, e só volta a andar quando o jogador carrega em
"Continuar". Ao sair para o menu, ou ao fechar a página, não se recupera tempo
nenhum ao voltar: o reino continua do ponto exato onde ficou. Para isso a
gravação leva os contadores a meio (bens, ciclos, relógio das vilas, stock do
mercado) com duas casas decimais, a vista (vw: a casa ao centro do ecrã, o
zoom e a rotação) e a velocidade do relógio (sp).
A pontuação no quadro é a prosperidade (moedas, bens ao preço de referência, edifícios e castelo), enviada ao sair e ao subir o castelo — só quando é melhor do que a última enviada, e limitada ao máximo que o servidor aceita (999 999).
Uma melodia calma em flauta de pã com uma batida de bateria suave e um
baixo dedilhado (js/music.js, no motor comum), toda
sintetizada com osciladores e ruído — sem ficheiros de áudio, como os efeitos de
js/audio.js, com quem partilha o AudioContext. A flauta é um triângulo suavizado mais um seno na fundamental
(um tubo fechado só tem harmónicos ímpares), com um "chiff" de ruído no ataque,
um fio de sopro durante a nota e vibrato só depois de a nota assentar; a
bateria é bombo, tarola abafada, shaker em semicolcheias e uma virada de
timbalões no fim de cada frase; um reverb com uma resposta ao impulso gerada dá
a sala (o do motor). O tema está em ré dórico a 78 BPM, 20 compassos (pouco mais de um
minuto) em loop: A, A', B, B' e uma secção calma de notas longas com a bateria
a meio tempo. A partitura são as SECTIONS, um compasso por texto
('D5 3, E5 1, F5 2, A5 2': nota e duração em colcheias, - para pausa).
A música toca no menu e no reino e cala-se (com um fade) na pausa e com a
página escondida. Como o browser só deixa soar depois de um gesto, começa no
primeiro toque ou tecla, onde quer que seja. O botão Música da barra de topo
liga-a e desliga-a, e a escolha fica no aparelho (terrasDoReinoMusic_v1); os
efeitos sonoros continuam a tocar.
BUILDINGS (js/config.js): custo, escalão, trabalhadores,
receita (recipe) e, se precisar, vizinhos (near) ou sítio (site).STATIC (e, se mexer, em LIVE) no js/sprites.js.js/economy.js.A gravação guarda o tipo pelo índice em BUILDINGS: acrescenta-se sempre no
fim da lista, senão as gravações antigas trocam os edifícios uns pelos outros.
Versão 1.0.0, lançada a 5 de outubro de 2026. Esteve em testes desde 26 de setembro, fora do catálogo (ver a nota do Terras do Reino). As pontuações desse período já estavam a ser guardadas e aparecem no quadro desde o lançamento.
O jogo da memória de sempre, com animais: as cartas estão viradas para baixo,
viram-se duas de cada vez e, se forem iguais, o par fica à vista. É o primeiro
jogo da arcada todo em DOM — as cartas são <button>, viradas com uma rotação
3D de CSS (css/board.css) — e sem canvas nenhum.
Os níveis são só um número de cartas. O primeiro tem 4 e cada nível traz
mais 2, até às 50 do nível 24 (FIRST_LEVEL_CARDS, CARDS_STEP e MAX_CARDS
no js/config.js; o LEVELS do js/levels.js sai daí). Os animais saem à
sorte de cada vez que se começa um nível (dealCards em js/board.js), por
isso repetir um nível nunca é decorar onde estavam as cartas. No início as
cartas ficam uns instantes à vista ("Olha bem…"), mais tempo quanto maior o
tabuleiro, com teto de 4 s.
O tamanho das cartas não é fixo — é o maior que cabe. A cada mudança de
tamanho do palco (um ResizeObserver: rodar o telemóvel, a barra do browser a
esconder-se), o fitGrid do js/layout.js experimenta todas as contagens de
colunas — e duas formas de cartão, um pouco mais alto do que largo ou
quadrado — e fica com a que dá os cartões com mais área na área livre. As 50
cartas do último nível ficam num 5 × 10 ao alto (cartões quadrados de uns 59px
num telemóvel de 390 × 780) e num 13 × 4 ao comprido, sem nenhuma das grelhas
estar escrita em lado nenhum. O teto das 50 vem daí: mais um degrau e os
cartões desciam abaixo dos 55px num telemóvel comum. A última fila, quando não está cheia, fica centrada: o tabuleiro é um
flex com quebra de linha, não uma grelha. O smoke-test confirma, nos vinte e
quatro níveis e em três tamanhos de ecrã, que nenhuma carta sai do ecrã, fica por
baixo do HUD ou por cima de outra.
A escolha de nível é o carrossel do Maze Run. Os mesmos cartões de cinco
linhas (número, nome, cartas, estado e estrelas) e as mesmas páginas — 2 × 2 ao
alto, 3 × 1 ao comprido, 3 × 2 no computador —, com o carrossel partilhado do
SDK (lib/arcade/carousel.js) e a folha css/carousel.css gémea da do Maze
Run (ver O carrossel dos níveis). Onde o Maze Run
desenha o labirinto, aqui vai o tabuleiro em miniatura: as cartas do nível na
grelha que o fitGrid lhes dá num telemóvel ao alto, com a patinha das costas
em cada uma (o símbolo #paw no index.html).
Cada nível tem a sua cor. O accent de cada nível (ACCENTS em
js/levels.js) condiz com o nome — a Colmeia é cor de mel, o Oceano é azul, a
Arca é roxa — e passa a ser o acento do jogo inteiro enquanto o nível está
apontado ou a ser jogado: o applyLevelAccent do js/level.js escreve-o em
--level-accent no <html>, e o --accent e as cores das cartas do
css/theme.css descem dele (com color-mix para os tons escuros). Botões,
título, barra de progresso, costas e moldura das cartas mudam todos juntos. As
cores são todas claras de propósito: o texto escuro dos botões e a patinha
branca têm de se ler em cima de qualquer uma. Nos cartões do carrossel, cada
um leva a cor do seu nível em --card-accent.
Estrelas pelos erros, pontos pelo resto. Um erro é uma jogada de duas cartas sem par. ★ por concluir, ★★ com tantos erros como pares ou menos, ★★★ com metade disso. É de propósito que o tempo não conta para as estrelas — é um jogo de memória, e devagar mas certo também é jogar bem. O tempo conta nos pontos: 100 por par, 5 por segundo abaixo do tempo-alvo (5 s por par), menos 20 por erro, com um chão de 25 por par. Ao quadro vai a soma da melhor marca de cada nível, como no Maze Run.
As ilustrações são ficheiros, não emoji. Cada aparelho desenha os emoji à
sua maneira (e há os que ainda não têm o burro), e num jogo de pares a mesma
carta tem de ter a mesma cara em todo o lado. São os SVG do
Fluent Emoji da Microsoft, estilo
"Color", licença MIT (assets/animais/LICENSE.txt), passados pelo svgo. Há
46 animais em js/animals.js, os da quinta primeiro; não há andorinha nem
faisão no Fluent Emoji, e o passarinho e o galo fazem as vezes deles.
Acrescentar um animal é pôr o SVG em assets/animais/<id>.svg e uma linha no
ANIMALS — a galeria do "Como jogar" monta-se a partir da mesma lista, e o
smoke-test verifica que todas as ilustrações carregam.
Uma melodia calma na marimba (xilofone de madeira), com a mão esquerda em arpejos e uma bateria
de vassouras a acompanhar (js/music.js), toda sintetizada — sem ficheiros de
áudio, como os efeitos de js/audio.js, com quem partilha o AudioContext. O
motor é o comum (agendador, reverb, compressor); aqui ficam
o instrumento e a partitura. A marimba são ondas sinusoidais — a fundamental,
um harmónico duas oitavas acima e outro agudo só na pancada da baqueta — que
decaem sozinhas desde o ataque, sem sustentação (um som sustentado e rico em
harmónicos soaria a metal), mais devagar nas notas graves; as notas longas da
melodia vão em rulo. A música fica bem por baixo dos efeitos (volume 0,22). A bateria é bombo, vassoura na
tarola, pratos de choque fechados e dois timbalões a fechar as frases que voltam
a casa. O tema está em fá maior a 76 BPM, 20 compassos (pouco mais de um minuto)
em loop: A, A', B, B' e uma secção calma de acordes em bloco com a bateria a
meio tempo. A partitura são as SECTIONS: a melodia um compasso por texto
('C5 2, F5 2, A5 3, G5 1': nota e duração em colcheias, - para pausa, b
e # nas alterações) e o acorde de cada compasso ('Bb C' muda a meio).
A música toca nos menus, no tabuleiro e nos resultados, e cala-se (com um fade)
na pausa e com a página escondida. Começa no primeiro toque ou tecla, onde quer
que seja. O botão Música da barra de topo liga-a e desliga-a, e a escolha
fica no aparelho (memoriaAnimalMusic_v1); os efeitos sonoros continuam a
tocar. Num telemóvel estreito o botão fica só com a nota, para caberem os três.
Versão 1.0.0, lançada a 10 de outubro de 2026. Esteve em testes desde 5 de outubro, fora do catálogo (ver a nota do Terras do Reino). As pontuações desse período já estavam a ser guardadas e aparecem no quadro desde o lançamento.
O primeiro jogo de plataformas da arcada. Escolhe-se um de quatro bichos — cabeça grande, olhos enormes, corpo pequenino — e corre-se para a direita até ao frasco das guloseimas, no fim do percurso, a apanhar as guloseimas pelo caminho. Ganha quem lá chega depressa e com mais guloseimas: as três estrelas de cada nível são chegar ao fim, trazer as guloseimas todas e chegar dentro do tempo-alvo — independentes, para quem corre sem as apanhar todas também ter a sua.
Tudo é desenhado no canvas, sem imagens: os bichos (js/animals.js), os
guardiões, as plantas, as bombas e os enfeites (js/sprites.js) e os tiles
(js/tiles.js, pintados uma vez por tamanho e depois só carimbados). Os quatro
bichos têm a mesma física de propósito — o quadro é um só, e escolher a raposa
não pode ser escolher uma vantagem. A escolha fica no aparelho
(gulosinhosAnimal_v1).
Acrescentar um nível é acrescentar uma entrada ao LEVELS (js/levels.js):
o cenário, a semente, a dificuldade, os desafios que pode ter, os que apresenta
(aparecem mais vezes) e quantos segundos deve durar. O buildWorld de
js/world.js monta o percurso da esquerda para a direita, troço a troço — um
buraco, uma escada, uma geleia a patrulhar, uma parede de caixotes com uma bomba
à frente —, escolhidos à sorte pela semente, até o tempo estimado chegar ao da
receita. Por isso os níveis duram todos mais ou menos o mesmo, sejam feitos de
troços rápidos ou de troços de esperar. Há duas bandeiras por nível, a um terço
e a dois terços, onde se recomeça depois de cair num buraco.
Cada troço é escrito a contar com o salto. Com a física do js/config.js, o
salto sobe ~2,8 tiles e, a correr, vai ~4,8 tiles mais longe; daí os limites do
gerador — degraus de 2 no máximo (MAX_STEP_UP), buracos de 4 (MAX_GAP), de 3
se o outro lado for um degrau acima — e as guloseimas só onde se chega. Quem
mudar a física ou escrever um troço novo tem de refazer essas contas; o
smoke-test verifica-as nos doze níveis (gulosinhos-percursos) e joga cada
mecânica num pedaço de percurso feito à medida (gulosinhos-mecanicas).
O tempo da receita é a conta do gerador, não um cronómetro: o tempo-alvo é
15% acima dela. Para saber quanto um nível demora a sério há um piloto
automático, tools/games/gulosinhos-bot.mjs, que joga cada nível com a física
do jogo e diz o tempo, as guloseimas e onde levou pancada. Ele só corre, sem se
desviar para as guloseimas, e faz os níveis em 1:00 a 1:45; quem joga com calma
leva os 1:30 a 2:00 que o jogo pede.
cd tools/games
node gulosinhos-bot.mjs # os doze níveis
node gulosinhos-bot.mjs 3,7 # só alguns
Os cartões dos níveis mostram o próprio nível em ponto pequeno: uma
miniatura desenhada num canvas pelo mesmo código do jogo (paintLevelShot em
js/render.js), apontada ao primeiro sítio do percurso onde aparece o desafio
que o nível apresenta — a planta do Pomar, a parede de caixotes do Bosque, o
elevador da Gruta —, com as plantas de boca aberta e as bombas a contar. Onde
apontar sai do registo dos troços que o gerador deixa no mundo (segments) e
da tabela FEATURE_SEGMENTS do js/menu.js. São canvas e não imagens, por
isso o carrossel volta a pintá-las sempre que refaz as páginas (o onRender).
| desafio | o que faz | na receita |
|---|---|---|
| Geleia | guarda as guloseimas, a patrulhar; cair-lhe em cima pisa-a | jelly |
| Ouriço | como a geleia, mas pica por cima: salta-se por cima dele | hedgehog |
| Abelha | sobe e desce por cima do caminho; passa-se quando está lá em cima | bees |
| Planta carnívora | sai do vaso de tempos a tempos, de boca aberta | plants |
| Bomba-relógio | começa a contar quando alguém se chega perto e rebenta passados 2,4 s | bombs |
| Picos | no chão; os compridos atravessam-se por plataformas | spikes |
| Pilares | saltinhos por cima de um buraco comprido | pillars |
| Mola | atira para cima de uma parede que nenhum salto vence | springs |
| Elevador | sobe a um planalto; ao lado é buraco | lifts |
| Plataforma | vai e vem por cima de um buraco largo de mais para saltar | movers |
| Ponte de bolachas | desfaz-se pouco depois de se lhe pôr o pé em cima | crumble |
Decisões que não são óbvias:
breakCrates em js/level.js). Uma parede de caixotes nunca fica meio de pé,
por isso o percurso nunca fica trancado. A bomba fica encostada à parede: para
fugir dela basta recuar, sem ter de passar por cima dela.MOVER_DWELL) e sobe-se
para elas sem saltar quando estão um nadinha acima do chão (STEP_ONTO em
js/physics.js): sem isso, apanhar um elevador que já arrancou era cair no
buraco ao lado.SUBSTEP): a mola atira a 26
tiles por segundo, e um passo de um fotograma atravessava um tile.COYOTE_TIME e
JUMP_BUFFER). Largar o botão a subir encurta o salto.O tile é o maior que deixa ver 11,5 tiles na vertical e 11 na horizontal
(js/camera.js): ao comprido vê-se um bom bocado do caminho à frente; ao alto o
percurso cabe todo na vertical, com céu por cima. A câmara segue o bicho com um
pouco de atraso e olha para o lado para onde ele corre.
No computador: setas ou A/D para correr, espaço, ↑, W, Z ou K para saltar, Esc
ou P para a pausa. No telemóvel, três botões desenhados no canvas (buttonRects
em js/input.js): ← e → à esquerda, o salto à direita. Ao alto ficam numa faixa
só deles por baixo do mundo, para os polegares não taparem o chão; ao comprido,
por cima do mundo, nos cantos. Aparecem num aparelho de toque, ou ao primeiro
toque no ecrã.
Uma marcha saltitona de desenho animado (js/music.js), toda sintetizada: a
melodia num "pizzicato" de brinquedo (onda quadrada abafada por um filtro que
fecha logo a seguir ao ataque, mais um seno uma oitava acima), baixo aos saltos,
acordes de cavaquinho nos contratempos e bateria leve. Dó maior a 132 BPM, 20
compassos em loop: A, A', B, B' e uma ponte calma só com o bombo. O motor é o
comum; o botão Música liga-a e desliga-a
(gulosinhosMusic_v1).
Versão 1.0.0, lançada a 24 de outubro de 2026. Esteve em testes desde 22 de outubro, fora do catálogo (ver a nota do Terras do Reino). As pontuações desse período já estavam a ser guardadas e aparecem no quadro desde o lançamento. Uma primeira versão chegou a ser publicada a 16 de outubro sem passar pelos testes; esta foi reescrita de raiz, e o progresso dessa (48 níveis gerados, outra numeração) não passa para esta: o
VERSIONdojs/progress.jssubiu para 2.
Bola, raquete e tijolos, como na máquina de salão. Cada nível acaba quando não
sobra nenhum tijolo que se parta; as três estrelas são acabar, acabar sem perder
vidas e acabar dentro do tempo-alvo (par, que sai dos golpes que os tijolos
pedem: PAR_BASE + PAR_PER_HIT por golpe, em js/config.js).
Tudo é medido em unidades do campo, nunca em píxeis do ecrã. O campo tem
13 colunas de tijolos de 32 × 16 unidades (416 de largura, 640 de altura), com
paredes à esquerda, à direita e no teto e a faixa do marcador por cima. O
js/field.js é o único sítio que olha para o ecrã: calcula a escala com que a
caixa inteira cabe, e o desenho passa por uma única transformação
(applyFieldTransform). A física, os tijolos, a raquete, o marcador e a grelha
do chão (as colunas dos tijolos) vivem no mesmo referencial, e por isso não há
como ficarem desalinhados — que era o defeito da primeira versão, que media
umas coisas em unidades e outras em píxeis.
Ao alto, o campo fica por baixo da barra de topo. Ao comprido sobra largura e falta altura, por isso o campo sobe até ao cimo do ecrã sempre que cabe à esquerda dos botões da barra sem lhes tocar (o smoke-test confirma); à volta fica o céu do mundo.
O cenário (céu, paredes, chão, vidro do marcador) não muda a jogar, e é
desenhado uma vez num canvas à parte e carimbado a cada frame. Os tijolos
também: cada aspeto (tipo, cor, golpes que faltam) é desenhado uma vez à
resolução do ecrã (brickSprite em js/draw.js).
Acrescentar um nível é acrescentar uma grelha ao DESIGNS (js/levels.js):
13 letras por linha, e o que se vê no ficheiro é o que aparece no campo.
| letra | tijolo |
|---|---|
w o c g r b p v y | de cor; um golpe, e vale mais quanto mais "quente" a cor |
S | prata: dois golpes |
T | titânio: três golpes |
E | TNT: rebenta e parte os oito vizinhos (uma TNT vizinha rebenta também) |
# | aço: não se parte |
. | buraco |
Os níveis estão em mundos de seis (WORLDS), cada um com a sua cor; os
quatro primeiros apresentam um tijolo novo e os dois últimos (Cosmos e
Supernova) juntam-nos todos, mais depressa.
São 36, e um nível novo entra de seis em seis. O carrossel mostra 4
cartões por página ao alto, 6 no computador e 3 deitado, e só um múltiplo de
12 enche a última página nos três — com 30, a última página ao alto tinha dois
cartões, e é por isso que os outros jogos com níveis têm 12, 24 e 48. Os
mundos novos entram no fim da lista, para o número dos níveis que já existem
não mudar (o progresso guarda as marcas pelo número). Mesmo assim, uma página
incompleta já não estica os cartões: as filas de cada página vêm contadas
(grid-template-rows com --page-rows, em css/carousel.css), e a que falta
fica vazia. A velocidade da bola e a largura da raquete não se escrevem nível a
nível: andam em linha reta do primeiro ao último (levelTuning). Os nomes são
de uma palavra, pela regra dos cartões do carrossel.
Duas regras para o aço, aprendidas a jogar: nunca fechar um corredor de uma fila só (a bola não consegue andar de lado lá dentro, e o que estiver ao fundo fica por partir), e nunca uma abertura de uma coluna para chegar a tijolos que só se alcançam por ela. Ao mexer nos níveis, o piloto automático (abaixo) diz se todos se acabam.
js/physics.js faz andar a bola aos bocadinhos de BALL_SUBSTEP unidades —
menos do que o raio — e resolve no máximo um tijolo por bocadinho, como um
círculo contra um retângulo: numa face espelha nesse eixo, num canto espelha na
direção do canto. Assim nenhuma bola atravessa um tijolo num frame lento nem
parte dois de uma vez por uma esquina. Na raquete, o ressalto depende só de
onde a bola bate (no meio sobe a direito, nas pontas sai inclinada até
PADDLE_MAX_ANGLE) — é isso que dá pontaria. Uma bola quase horizontal é
endireitada (BALL_MIN_VY), para não ficar presa a ricochetear de parede em
parede.
A bola sai sozinha ao fim de AUTO_LAUNCH segundos na raquete.
Caem de alguns tijolos partidos (CAPSULE_CHANCE) e cada uma só aparece a
partir de um nível (from, em js/powerups.js):
| cápsula | poder |
|---|---|
| A | raquete larga |
| D | bola devagar (com relógio) |
| M | cada bola dá mais duas |
| + | vida extra |
| C | raquete-íman: a bola cola e sai quando se lança |
| L | laser: dois disparos a cada LASER_INTERVAL (com relógio) |
A, C e L são da raquete e trocam umas pelas outras; perder a bola leva todos os poderes.
Para afinar níveis sem os jogar à mão, o smoke-test tem um cenário de mecânicas
(TNT em cadeia, prata, aço, cápsulas, perder as vidas), e qualquer guião pode
correr a física do jogo a 60 passos por segundo dentro de um page.evaluate:
startLevel, game.paused = true (para o ciclo da página não mexer) e
updateLevel(1 / 60) em ciclo, com input.pointerX a seguir a bola. Foi assim
que se apanharam os dois níveis que não se acabavam e que se mediu o tempo-alvo.
Um tema de salão de jogos em synthwave (js/music.js): lá menor a 136 BPM,
com baixo de dentes de serra a saltar à oitava, arpejo em semicolcheias com eco
de lado a lado, solo de dois dentes de serra e uma quadrada com vibrato, pad na
pausa e bateria a quatro tempos. 32 compassos (perto de um minuto) em loop:
introdução, tema, segunda parte mais acima, pausa sem bombo a crescer num rufo,
e o tema com o solo dobrado à oitava. Tem o volume da do Pixel Racing (RMS
perto de −21 dBFS no render-music.mjs) — a primeira versão tinha uma volta de
7 segundos a −42 dBFS. O botão Música liga-a e desliga-a
(quebraTijolosMusic_v1).
Um vira, como os dos ranchos folclóricos: três tempos a 168 BPM, com
acordeão, bandolim, cavaquinho, baixo e a bateria de rancho
(js/music.js, no motor comum). A melodia parte do motivo
da música antiga da tasca (ré, fá, sol, fá, lá, sol, fá, ré), agora a dançar: a
primeira frase em ré menor, a segunda no fá maior ao lado, e o Lá maior a trazer
tudo de volta. São 32 compassos (pouco mais de meio minuto) em loop: A e B com o
acordeão, A com o bandolim a cantar, e B com os dois juntos.
Toca nos menus e no turno; cala-se (com um fade) na pausa, com a página
escondida e com o 🔊 do cabeçalho desligado. O botão da nota, ao lado, liga e
desliga só a música (tascaDoZeMusic_v1).
Um tema alegre em allegro (sol maior, 132 BPM) com guitarra, piano,
baixo e bateria (js/music.js), sintetizado no motor comum
como o do Terras do Reino e o da Memória Animal, e a partilhar o AudioContext
com o motor do carro e os efeitos de js/audio.js.
STRUM, 'B-BC-CBC': para
baixo, para cima, nada), com os desenhos de guitarra de cada acorde em
CHORDS; no B faz "chug" abafado. A guitarra solo usa cordas que soam mais
tempo, mais saturação e vibrato nas notas longas.A forma são 28 compassos (pouco menos de um minuto) em loop: A com o piano, B
com a guitarra solo, uma pausa de meio tempo que cresce num rufo de tarola, e o
tema outra vez com piano e guitarra juntos e os pratos em semicolcheias. A
partitura escreve-se como nos outros jogos ('D5 1, G5 1, B5 1, D6 2, …') nas
SECTIONS, onde cada secção diz também quem canta (lead), como toca a
guitarra de ritmo (rhythm) e a bateria (drums).
Toca nos menus, na contagem, na corrida e nos resultados; cala-se (com um fade)
na pausa e com a página escondida; começa no primeiro toque ou tecla. O botão
Música da barra de topo liga-a e desliga-a, e a escolha fica no aparelho
(pixelRacingMusic_v1); o motor e os efeitos continuam a tocar.
Quem afixa a arcada ao ecrã principal do telemóvel nunca faz Ctrl+F5 — se o
browser guardar um js/ velho, fica com ele. Para isso não acontecer, o
Web/CachePolicy.cs carimba o Cache-Control de todas as respostas:
| resposta | Cache-Control |
|---|---|
URL com ?v= (o que o @Assets[] do Blazor gera) | public, max-age=31536000, immutable |
| tudo o resto | no-cache |
no-cache não é "não guardes", é "guarda mas pergunta antes de usar": o browser
revalida com o ETag e recebe um 304 vazio quando nada mudou. Por isso os
ficheiros dos jogos (games/<slug>/index.html, css/, js/), que não têm
versão no URL, aparecem sempre atualizados a seguir a um deploy sem custo de
tráfego quando não mudaram.
Consequência prática ao acrescentar ficheiros a um jogo: não é preciso fazer nada. Não há lista de assets para manter nem hash para escrever à mão.
O porquê desta escolha, e o que faria mudá-la, está em docs/cache-do-browser.md.
O workflow .github/workflows/main_marquitos-arcade.yml publica src/MarquitosArcade e faz deploy para o App Service marquitos-arcade a cada push em main. O site é servido em arcade.marquitos.pt, um domínio próprio apontado a esse App Service.
O domínio próprio não é cosmético. O Azure já não dá a Web Apps novas um endereço
simples: gera um sufixo único por app, do género
marquitos-arcade-<sufixo>.spaincentral-01.azurewebsites.net. Se a Web App for
recriada, esse endereço muda. Por isso o README não o refere, e o workflow também
não: identifica o destino pelo nome do App Service (app-name: 'marquitos-arcade'),
que se mantém. Recriar a Web App com o mesmo nome não obriga a mexer no deploy;
só é preciso reapontar o DNS.
Para a base de dados sobreviver a deploys, a connection string (ConnectionStrings__DefaultConnection) deve apontar para um caminho persistente do App Service (ex.: /home/data/app.db), não para dentro da pasta de conteúdo publicada.
gulosinhos/: Gulosinhos, jogo de plataformas em tiles — um de quatro bichos fofinhos (gatinha, coelhinho, pandinha e raposinha) corre até ao frasco no fim de cada percurso a apanhar guloseimas, entre geleias que guardam os doces, ouriços, abelhas, plantas carnívoras, bombas-relógio, picos, molas, elevadores, plataformas que andam e pontes de bolachas. A câmara segue o bicho. Doze níveis, cada um com 1:30 a 2:00 de percurso, que não estão desenhados à mão: saem de uma semente e de uma receita (LEVELS em js/levels.js, gerados por buildWorld em js/world.js). Ver Gulosinhos.quebra-tijolos/: Quebra-Tijolos, o clássico de bola e raquete — partir os tijolos todos sem deixar cair a bola, com cápsulas que caem de alguns tijolos (raquete larga, íman, laser, bola devagar, três bolas e vida extra). Trinta e seis níveis desenhados à mão em texto (LEVELS em js/levels.js), em seis mundos de seis; os quatro primeiros apresentam cada um um tijolo novo (prata, aço, TNT e titânio) e os dois últimos juntam-nos. Ver Quebra-Tijolos.memoria-animal/: Memória Animal, jogo da memória com cartas de animais — virar duas de cada vez e encontrar os pares. Vinte e quatro níveis, de 4 cartas (2 pares) a 50 (25 pares), mais 2 por nível; as cartas encolhem à medida que o tabuleiro cresce, para caberem todas no ecrã do telemóvel. Níveis desbloqueados e estrelas pelo mesmo cliente de progresso do Maze Run. Ver Memória Animal.pixel-racing/: Pixel Racing, corrida simples em qualquer uma das doze pistas ou campeonato com todas, com pontuação via /api/scores/pixel-racing. O menu tem dois passos: o primeiro ecrã pergunta só o nome (a quem não tem sessão iniciada) e o modo; o segundo tem duas páginas no mesmo painel — a pista (no campeonato, a lista das que se vão correr) e a dificuldade, e depois a garagem, com o carro e a cor. As pistas e os carros escolhem-se em carrosséis — o partilhado do SDK (lib/arcade/carousel.js), com a folha css/carousel.css do jogo a dizer quantos cabem por página: pistas 2 × 2 ao alto, 3 × 1 ao comprido e 3 × 2 no computador; carros sempre numa fila só, dois por página. Os cartões dos carros levam miniaturas em canvas, que se perdem quando o carrossel refaz as páginas (ao rodar o aparelho); a opção onRender do createCarousel é onde se voltam a desenhar, e onde se repõe o cartão escolhido. Há quatro carros (CAR_TYPES em js/config.js), do mais lento ao mais rápido: kart, jeep, 911 GT e Fórmula 1, cada um com multiplicadores próprios de velocidade, aceleração, curva, aderência, andamento fora de pista e carga de drift. Cada corrida é de um tipo só — os adversários correm com o carro que o jogador escolheu —, por isso escolher o carro é escolher a categoria; no modo campeonato, é escolher qual dos quatro campeonatos (karts, jeeps, GT, Fórmula 1) se corre, e cada um corre as doze pistas, da mais fácil à mais difícil. O desenho dos quatro está em js/carsprites.js, partilhado pela corrida e pelas miniaturas da garagem, e cabe no mesmo retângulo de colisão (CAR_LEN × CAR_W). As pistas são geradas por buildTrack de duas maneiras: a partir de uma superelipse com harmónicos (as seis originais) ou de uma lista de pontos de controlo por onde passa uma Catmull-Rom centrípeta (points, as seis com ganchos, serpentinas e esses, que nenhuma superelipse dá). A dificuldade é medida no traçado (corneringProfile) em vez de escrita à mão — a curva mais fechada comparada com o raio que se faz a fundo, mais o quanto a pista vira por cada 1000 unidades —, e é por ela que TRACKS se ordena e que sai o grau de perícia do cartão. Ao desenhar uma pista por pontos, nenhuma curva deve ser mais apertada do que a meia largura da pista (senão a margem de dentro dobra-se) e troços diferentes têm de ficar a mais de duas vezes barrierAt um do outro, para as escapatórias não se tocarem. Além dos postos de turbo e das poças de óleo, cada pista tem uma ou duas rampas (placeRamps), postas sozinhas nas retas mais compridas, centradas numa das faixas e com 60% da largura dela: quem passa nelas com velocidade salta, segue a direito no ar (o volante só roda o nariz) e cresce com a altura do salto; ao aterrar há dois ou três saltinhos de suspensão, e aterrar torto custa velocidade. A cor sai da paleta única de CAR_COLORS e os adversários ficam com três das restantes, por isso nunca há dois carros da mesma cor na pista.--cover-bgGames em Components/Pages/Pontuacoes.razor para aparecer na página de pontuações, pela mesma ordem do catálogo.ROLESjs/walkers.jscropBUILDINGSHAPPY_LEISURE): ao fim do dia cada taberna serve 40 moradores (1
vinho por cada 10, até +4%), cada teatro 80 (até +3%) e cada
joalharia vende joias a 80 (1 joia por cada 40, até +3%), e cada um dá a
sua parte na proporção do povo servido (serves em BUILDINGS, com o bem
que gasta em uses).needs em CASTLE_LEVELS). O território deixa de crescer no
nível 4: logo a seguir começam as terras das vilas vizinhas.FLATTEN_COST), e passa a terra livre —
devolvendo alguma pedra (FLATTEN_STONE). As veias de ouro não se aplanam.
Limpar a área: uma árvore ou um rochedo no território corta-se ou
parte-se na ficha da casa, por moedas (CLEAR_COST), e a casa fica livre.
Não se aproveita a madeira nem a pedra — é só o custo do trabalho. A
gravação lembra-se das casas limpas (cr); um guarda-florestal perto pode
voltar a plantar lá.ROAD_COST por casa): no modo de estrada toca-se
onde começa e depois onde acaba cada troço, e o caminho contorna sozinho
edifícios, árvores, água e colinas (preferindo as estradas que já há). Onde
uma estrada encosta a dois edifícios, o povo anda a pé entre eles
(js/walkers.js): tanto mais gente quanto mais moradores
(RESIDENTS_PER_WALKER). As vilas calcetam as ruas à volta de cada
edifício que fazem e têm a sua própria gente na rua. À volta do castelo fica
uma praça de uma casa onde só há estradas.js/input.js). Um toque só conta se o dedo
quase não se mexeu — largar o dedo no fim de um arrasto não constrói nada.