@aksp/opencrew 1.6.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +50 -5
- package/package.json +1 -1
- package/templates/AGENTS.md +20 -6
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/escritorio/animacao.js +64 -0
- package/templates/_opencrew/core/escritorio/app.js +137 -0
- package/templates/_opencrew/core/escritorio/cena.js +132 -0
- package/templates/_opencrew/core/escritorio/demo.js +79 -0
- package/templates/_opencrew/core/escritorio/escala.js +27 -0
- package/templates/_opencrew/core/escritorio/index.html +166 -0
- package/templates/_opencrew/core/escritorio/modelo-agentes.js +93 -0
- package/templates/_opencrew/core/escritorio/modelo-estado.js +71 -0
- package/templates/_opencrew/core/escritorio/modelo-mesas.js +81 -0
- package/templates/_opencrew/core/escritorio/modelo-pagina.js +95 -0
- package/templates/_opencrew/core/escritorio/modelo-textos.js +65 -0
- package/templates/_opencrew/core/escritorio/modelo-visao.js +91 -0
- package/templates/_opencrew/core/escritorio/modelo.js +29 -0
- package/templates/_opencrew/core/escritorio/painel.js +120 -0
- package/templates/_opencrew/core/escritorio/quadro.js +106 -0
- package/templates/_opencrew/core/escritorio/rota.js +62 -0
- package/templates/_opencrew/core/escritorio/rotulos.js +78 -0
- package/templates/_opencrew/core/escritorio/sprites-mesa.js +122 -0
- package/templates/_opencrew/core/escritorio/sprites-sala.js +92 -0
- package/templates/_opencrew/core/escritorio/sprites.js +187 -0
- package/templates/_opencrew/core/prompts/build.prompt.md +3 -3
- package/templates/_opencrew/core/prompts/repair.prompt.md +7 -12
- package/templates/_opencrew/core/runner.pipeline.md +45 -128
- package/templates/_opencrew/core/scripts/escritorio/leitura.mjs +31 -0
- package/templates/_opencrew/core/scripts/escritorio/porta.mjs +98 -0
- package/templates/_opencrew/core/scripts/escritorio/projeto.mjs +29 -0
- package/templates/_opencrew/core/scripts/escritorio/servidor.mjs +78 -0
- package/templates/_opencrew/core/scripts/escritorio.mjs +117 -0
- package/templates/_opencrew/core/scripts/estado/argumentos.mjs +61 -0
- package/templates/_opencrew/core/scripts/estado/arquivo.mjs +53 -0
- package/templates/_opencrew/core/scripts/estado/decisao.mjs +56 -0
- package/templates/_opencrew/core/scripts/estado/elenco.mjs +58 -0
- package/templates/_opencrew/core/scripts/estado/nucleo.mjs +113 -0
- package/templates/_opencrew/core/scripts/estado/preferencia.mjs +24 -0
- package/templates/_opencrew/core/scripts/estado.mjs +96 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Núcleo puro da página do escritório: tudo o que ela desenha e escreve sai daqui, calculado a
|
|
2
|
+
// partir do estado que o servidor devolve. Nenhum módulo deste núcleo toca no DOM, lê relógio ou
|
|
3
|
+
// usa a rede: a hora atual, o "reduzir movimento" e o `?demo` entram por parâmetro, e por isso
|
|
4
|
+
// os mesmos módulos são testados direto no Node.
|
|
5
|
+
// Este arquivo é só a fachada: cada módulo irmão cuida de uma parte.
|
|
6
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regras 16 a 24, 28 e 29 (repositório do OpenCrew).
|
|
7
|
+
|
|
8
|
+
// Geometria: a tela de 320×180, o boneco de 16 e as 12 mesas.
|
|
9
|
+
export * from './modelo-mesas.js';
|
|
10
|
+
// Textos da página em PT-BR e o título da aba.
|
|
11
|
+
export * from './modelo-textos.js';
|
|
12
|
+
// Leitura do `state.json`, inclusive o da 1.6.x.
|
|
13
|
+
export * from './modelo-estado.js';
|
|
14
|
+
// Ação, monitor, balão e aparência de cada agente.
|
|
15
|
+
export * from './modelo-agentes.js';
|
|
16
|
+
// O modelo de uma leitura: `montarModelo`.
|
|
17
|
+
export * from './modelo-visao.js';
|
|
18
|
+
// Qual crew está na tela e a página inteira: `consultar`, `escolher`, `montarPagina`.
|
|
19
|
+
export * from './modelo-pagina.js';
|
|
20
|
+
// O roteiro da demonstração.
|
|
21
|
+
export * from './demo.js';
|
|
22
|
+
// A escala inteira do canvas, em pixels do dispositivo: `escala`.
|
|
23
|
+
export * from './escala.js';
|
|
24
|
+
// O caminho de uma entrega, pelos vãos entre as mesas: `rota`.
|
|
25
|
+
export * from './rota.js';
|
|
26
|
+
// O que anima, pela diferença entre duas leituras da mesma crew: `transicao`.
|
|
27
|
+
export * from './animacao.js';
|
|
28
|
+
// Pose, posição e monitor de cada boneco num instante: `quadro`.
|
|
29
|
+
export * from './quadro.js';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// O painel em volta do desenho: o nome da crew, o passo com a barra de progresso, os avisos, a
|
|
2
|
+
// faixa do checkpoint, a última passagem de bastão, o seletor de crews e a lista dos agentes
|
|
3
|
+
// (regras 22 a 24 e 29). Tudo o que vem do estado — nome, rótulo, mensagem, motivo — entra por
|
|
4
|
+
// `textContent`: é texto, nunca marcação. Cada parte só é reescrita quando muda.
|
|
5
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regras 18 e 20 a 24 (repositório do OpenCrew).
|
|
6
|
+
import { TEXTOS } from './modelo.js';
|
|
7
|
+
|
|
8
|
+
/** Os elementos de `index.html` que o painel escreve. */
|
|
9
|
+
const IDS = [
|
|
10
|
+
'pagina', 'crew', 'situacao', 'passo', 'barra', 'barra-feito', 'barra-atual', 'barra-resto', 'aviso', 'frase',
|
|
11
|
+
'faixa', 'etiqueta', 'parada', 'motivo', 'passagem', 'lista', 'seletor', 'seletor-caixa',
|
|
12
|
+
];
|
|
13
|
+
/** A situação da execução, por extenso. */
|
|
14
|
+
const SITUACAO = Object.freeze({ running: 'Em andamento', checkpoint: TEXTOS.aguardando, completed: 'Concluída', failed: 'Falhou' });
|
|
15
|
+
/** A marca de cada ação na lista: a forma muda junto com a cor. */
|
|
16
|
+
const MARCAS = Object.freeze({ idle: '○', working: '▶', checkpoint: '▲', done: '✓', skipped: '–', failed: '✕', 'sem-sinal': '?' });
|
|
17
|
+
|
|
18
|
+
/** Escreve o texto no elemento e o esconde quando não há o que mostrar. */
|
|
19
|
+
function escrever(elemento, texto) {
|
|
20
|
+
const novo = texto || '';
|
|
21
|
+
if (elemento.textContent !== novo) elemento.textContent = novo;
|
|
22
|
+
elemento.hidden = novo === '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Um elemento novo com classe e texto; `enfeite`: fora do alcance do leitor de tela. */
|
|
26
|
+
function criar(documento, marca, { classe, texto = '', enfeite = false }) {
|
|
27
|
+
const elemento = documento.createElement(marca);
|
|
28
|
+
elemento.className = classe;
|
|
29
|
+
elemento.textContent = texto;
|
|
30
|
+
if (enfeite) elemento.setAttribute('aria-hidden', 'true');
|
|
31
|
+
return elemento;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A linha de um agente: a cor da camisa (a mesma do boneco), o ícone, o nome, o status e o que fez.
|
|
36
|
+
* A marca segue o status do agente, não a ação do boneco (regra 18: no checkpoint a lista não
|
|
37
|
+
* muda); só "sem sinal" a troca.
|
|
38
|
+
*/
|
|
39
|
+
function criarLinha(documento, agente) {
|
|
40
|
+
const novo = (marca, opcoes) => criar(documento, marca, opcoes);
|
|
41
|
+
const item = novo('li', { classe: 'agente' });
|
|
42
|
+
const cor = novo('span', { classe: 'cor', enfeite: true });
|
|
43
|
+
const quem = novo('div', { classe: 'quem' });
|
|
44
|
+
const status = novo('span', { classe: 'status' });
|
|
45
|
+
const marca = agente.acao === 'sem-sinal' ? 'sem-sinal' : agente.status;
|
|
46
|
+
item.dataset.acao = marca;
|
|
47
|
+
cor.style.background = agente.aparencia.camisa ?? 'transparent';
|
|
48
|
+
status.append(novo('span', { classe: 'sinal', texto: MARCAS[marca], enfeite: true }), novo('span', { classe: 'texto', texto: agente.statusTexto }));
|
|
49
|
+
quem.append(novo('strong', { classe: 'nome', texto: agente.nome }), status);
|
|
50
|
+
if (agente.label) quem.append(novo('span', { classe: 'fez', texto: agente.label }));
|
|
51
|
+
item.append(cor, novo('span', { classe: 'icone', texto: agente.icone, enfeite: true }), quem);
|
|
52
|
+
return item;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function criarOpcao(documento, crew) {
|
|
56
|
+
return Object.assign(criar(documento, 'option', { classe: '', texto: crew }), { value: crew });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const textoDoPasso = ({ contagem, rotulo }) => (rotulo ? `${contagem} — ${rotulo}` : contagem);
|
|
60
|
+
const textoDaPassagem = ({ de, para, mensagem }) => `Última passagem de bastão: ${de} → ${para}${mensagem ? ` — ${mensagem}` : ''}`;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* O painel da página.
|
|
64
|
+
* @param {object} documento a página (`getElementById`, `createElement`, `title`)
|
|
65
|
+
* @param {(crew: string) => void} aoEscolher chamada quando o usuário troca de crew no seletor
|
|
66
|
+
* @returns {{ atualizar: (modelo: object) => void }} `atualizar` recebe o que `montarPagina` devolveu
|
|
67
|
+
*/
|
|
68
|
+
export function criarPainel(documento, aoEscolher) {
|
|
69
|
+
const el = Object.fromEntries(IDS.map((id) => [id, documento.getElementById(id)]));
|
|
70
|
+
const escrito = { crews: '', agentes: '' };
|
|
71
|
+
el.seletor.addEventListener('change', () => aoEscolher(el.seletor.value));
|
|
72
|
+
|
|
73
|
+
/** A barra em três partes: passos feitos, o passo atual e o que falta. */
|
|
74
|
+
function progresso({ passo, execucao }) {
|
|
75
|
+
const total = passo?.total ?? 0;
|
|
76
|
+
el.barra.hidden = total === 0;
|
|
77
|
+
if (!total) return;
|
|
78
|
+
const feitos = execucao === 'completed' ? total : Math.min(total, Math.max(0, passo.atual - 1));
|
|
79
|
+
const atual = feitos < total && passo.atual >= 1 ? 1 : 0;
|
|
80
|
+
el.barra.style.setProperty('--total', String(total));
|
|
81
|
+
[feitos, atual, total - feitos - atual].forEach((parte, i) => {
|
|
82
|
+
el[['barra-feito', 'barra-atual', 'barra-resto'][i]].style.flexGrow = String(parte);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** O seletor só aparece com mais de uma crew (regra 22). */
|
|
87
|
+
function seletor({ crews, crew }) {
|
|
88
|
+
const novo = JSON.stringify(crews);
|
|
89
|
+
el['seletor-caixa'].hidden = crews.length < 2;
|
|
90
|
+
if (novo !== escrito.crews) el.seletor.replaceChildren(...crews.map((nome) => criarOpcao(documento, nome)));
|
|
91
|
+
escrito.crews = novo;
|
|
92
|
+
if (crews.includes(crew) && el.seletor.value !== crew) el.seletor.value = crew;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A lista traz todo agente, com ou sem mesa: é ela que vale para o leitor de tela (regra 24). */
|
|
96
|
+
function lista({ agentes }) {
|
|
97
|
+
const novo = JSON.stringify(agentes.map((a) => [a.id, a.nome, a.icone, a.acao, a.statusTexto, a.label, a.aparencia.camisa]));
|
|
98
|
+
if (novo !== escrito.agentes) el.lista.replaceChildren(...agentes.map((agente) => criarLinha(documento, agente)));
|
|
99
|
+
escrito.agentes = novo;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function atualizar(modelo) {
|
|
103
|
+
if (documento.title !== modelo.titulo) documento.title = modelo.titulo;
|
|
104
|
+
el.pagina.dataset.execucao = modelo.execucao ?? '';
|
|
105
|
+
el.etiqueta.hidden = modelo.modo !== 'demo';
|
|
106
|
+
escrever(el.crew, modelo.crew);
|
|
107
|
+
escrever(el.situacao, SITUACAO[modelo.execucao]);
|
|
108
|
+
escrever(el.passo, modelo.passo && textoDoPasso(modelo.passo));
|
|
109
|
+
escrever(el.aviso, modelo.aviso);
|
|
110
|
+
escrever(el.frase, modelo.frase);
|
|
111
|
+
escrever(el.faixa, modelo.faixa);
|
|
112
|
+
escrever(el.parada, modelo.parada);
|
|
113
|
+
escrever(el.motivo, modelo.motivo && `Motivo da falha: ${modelo.motivo}`);
|
|
114
|
+
escrever(el.passagem, modelo.passagem && textoDaPassagem(modelo.passagem));
|
|
115
|
+
progresso(modelo);
|
|
116
|
+
seletor(modelo);
|
|
117
|
+
lista(modelo);
|
|
118
|
+
}
|
|
119
|
+
return { atualizar };
|
|
120
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// O quadro de um instante (regra 28): a pose e a posição inteira de cada boneco, e o monitor de
|
|
2
|
+
// cada mesa, calculados pelo tempo decorrido. Nada aqui conta quadros: o mesmo modelo no mesmo
|
|
3
|
+
// instante dá sempre o mesmo quadro, e o percurso cujo tempo já passou aparece terminado.
|
|
4
|
+
// Puro: sem DOM, sem relógio, sem rede. A hora entra por parâmetro.
|
|
5
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regras 16, 18, 24 e 28 (repositório do OpenCrew).
|
|
6
|
+
import { ANDAR_MS, COMEMORACAO_MS, DIGITAR_MS, IDA_MS, VOLTA_MS } from './animacao.js';
|
|
7
|
+
import { pontoDaRota, rota } from './rota.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* As poses que `quadro` devolve. Cada uma é o molde do boneco com linhas trocadas (regra 16):
|
|
11
|
+
* `parado` é o molde; `digitar-a` e `digitar-b`, os braços no teclado; `mao`, o braço
|
|
12
|
+
* levantado; `andar-a` e `andar-b`, as pernas. O braço com papel vem à parte (`papel`), somado
|
|
13
|
+
* às pernas de quem anda.
|
|
14
|
+
*/
|
|
15
|
+
export const POSES = Object.freeze(['parado', 'digitar-a', 'digitar-b', 'mao', 'andar-a', 'andar-b']);
|
|
16
|
+
/** O monitor animado avança um passo a cada 250 ms: os 4 passos dão a volta em 1 segundo. */
|
|
17
|
+
export const MONITOR_MS = 250;
|
|
18
|
+
const PASSOS_DO_MONITOR = 4;
|
|
19
|
+
/** Quanto sobe, em pixels lógicos, quem comemora com o braço levantado. */
|
|
20
|
+
export const PULO = 2;
|
|
21
|
+
|
|
22
|
+
/** Em que passo de um ciclo de `total` cai o instante, quando o passo troca a cada `periodo`. */
|
|
23
|
+
function fase(ms, periodo, total = 2) {
|
|
24
|
+
return Number.isFinite(ms) && ms > 0 ? Math.floor(ms / periodo) % total : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Tempo desde o início de uma animação; `null` se não há, não começou ou já terminou. */
|
|
28
|
+
function decorrido(marca, duracao, agoraMs) {
|
|
29
|
+
const ms = marca ? agoraMs - marca.inicioMs : Number.NaN;
|
|
30
|
+
return ms >= 0 && ms < duracao ? ms : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** O âmbar pisca 1 vez por segundo (apaga na segunda metade); o aceso corre as linhas pelo passo. */
|
|
34
|
+
function monitorDe(agente, agoraMs) {
|
|
35
|
+
const monitorFase = agente.monitorAnimado ? fase(agoraMs, MONITOR_MS, PASSOS_DO_MONITOR) : 0;
|
|
36
|
+
const apagou = agente.monitor === 'ambar' && monitorFase >= PASSOS_DO_MONITOR / 2;
|
|
37
|
+
return { monitor: apagou ? 'apagado' : agente.monitor, monitorFase };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function poseNaMesa(acao, agoraMs, semMovimento) {
|
|
41
|
+
if (acao === 'checkpoint') return 'mao';
|
|
42
|
+
if (acao !== 'working') return 'parado';
|
|
43
|
+
return semMovimento || fase(agoraMs, DIGITAR_MS) === 0 ? 'digitar-a' : 'digitar-b';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** O boneco sentado na própria mesa, como o status manda. */
|
|
47
|
+
function naMesa(agente, agoraMs, semMovimento) {
|
|
48
|
+
const { id, indice, mesa, aparencia, acao } = agente;
|
|
49
|
+
return {
|
|
50
|
+
id, indice, mesa, aparencia, acao,
|
|
51
|
+
pose: poseNaMesa(acao, agoraMs, semMovimento),
|
|
52
|
+
papel: false,
|
|
53
|
+
espelhado: false,
|
|
54
|
+
sentado: true,
|
|
55
|
+
x: mesa.boneco.x,
|
|
56
|
+
y: mesa.boneco.y,
|
|
57
|
+
destino: null,
|
|
58
|
+
...monitorDe(agente, agoraMs),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Quem está no meio de uma entrega e o que muda no boneco dele; `null` se ninguém anda. */
|
|
63
|
+
function caminhada(entrega, agentes, agoraMs) {
|
|
64
|
+
const ms = decorrido(entrega, IDA_MS + VOLTA_MS, agoraMs);
|
|
65
|
+
if (ms === null) return null;
|
|
66
|
+
const [de, para] = [entrega.deId, entrega.paraId].map((id) => agentes.find((a) => a?.id === id));
|
|
67
|
+
if (!de?.mesa || !para?.mesa) return null;
|
|
68
|
+
const ida = rota(de.mesa.indice, para.mesa.indice);
|
|
69
|
+
const onde = ms <= IDA_MS ? pontoDaRota(ida, ms / IDA_MS) : pontoDaRota([...ida].reverse(), (ms - IDA_MS) / VOLTA_MS);
|
|
70
|
+
const pose = fase(ms, ANDAR_MS) === 0 ? 'andar-a' : 'andar-b';
|
|
71
|
+
return { indice: de.indice, boneco: { ...onde, acao: 'entregando', pose, papel: ms < IDA_MS, sentado: false, destino: para.mesa.indice } };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Comemora na própria mesa: braço levantado e um pulo, depois o molde, a cada 0,3 s. */
|
|
75
|
+
function comemorar(boneco, ms) {
|
|
76
|
+
const noAlto = fase(ms, DIGITAR_MS) === 0;
|
|
77
|
+
return { ...boneco, acao: 'comemorando', pose: noAlto ? 'mao' : 'parado', y: boneco.y - (noAlto ? PULO : 0) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* O que a cena pinta num instante: um boneco por agente com mesa, na ordem do elenco.
|
|
82
|
+
* @param {object} modelo o modelo de uma leitura; para haver entrega e comemoração, o que
|
|
83
|
+
* `transicao` devolveu
|
|
84
|
+
* @param {number} agoraMs a hora, em milissegundos, no mesmo relógio passado a `transicao`
|
|
85
|
+
* @returns {object[]} cada boneco: `{ id, indice, mesa, aparencia, acao, pose, papel, espelhado,
|
|
86
|
+
* sentado, x, y, destino, monitor, monitorFase }` · `acao`: a do modelo, ou `entregando`
|
|
87
|
+
* (durante os 4 s da entrega) ou `comemorando` (durante os 4 s da comemoração; quem foi pulado
|
|
88
|
+
* não comemora) · `pose`: uma de `POSES` · `papel`: leva o papel (só na ida) · `espelhado`:
|
|
89
|
+
* desenhar o molde virado para a esquerda · `sentado`: está na própria mesa · `x`, `y`: canto
|
|
90
|
+
* superior esquerdo do boneco, em pixel lógico inteiro · `destino`: índice da mesa de quem
|
|
91
|
+
* recebe, ou `null` · `monitor`: a cor a pintar agora (`apagado`, `aceso`, `ambar` ou
|
|
92
|
+
* `vermelho`) · `monitorFase`: 0 a 3, o passo das linhas do monitor aceso (0 se ele não anima)
|
|
93
|
+
* · com `modelo.reduzirMovimento`: pose fixa e todos sentados, sem entrega nem comemoração
|
|
94
|
+
*/
|
|
95
|
+
export function quadro(modelo, agoraMs) {
|
|
96
|
+
const todos = Array.isArray(modelo?.agentes) ? modelo.agentes : [];
|
|
97
|
+
const { entrega = null, comemoracao = null } = modelo?.animacao ?? {};
|
|
98
|
+
const semMovimento = Boolean(modelo?.reduzirMovimento);
|
|
99
|
+
const andando = semMovimento ? null : caminhada(entrega, todos, agoraMs);
|
|
100
|
+
const festa = semMovimento ? null : decorrido(comemoracao, COMEMORACAO_MS, agoraMs);
|
|
101
|
+
return todos.filter((agente) => agente?.mesa).map((agente) => {
|
|
102
|
+
const boneco = naMesa(agente, agoraMs, semMovimento);
|
|
103
|
+
if (andando?.indice === agente.indice) return { ...boneco, ...andando.boneco };
|
|
104
|
+
return festa !== null && agente.acao !== 'skipped' ? comemorar(boneco, festa) : boneco;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// O caminho de uma entrega (regra 28): do assento de quem entrega até a frente da mesa de quem
|
|
2
|
+
// recebe, calculado direto da grade, sem busca de caminho. O boneco sai pelo corredor de trás da
|
|
3
|
+
// própria mesa até o vão ao lado, anda pelo vão até o corredor da frente da mesa de destino e
|
|
4
|
+
// segue por ele. Só trechos horizontais e verticais, em pixel lógico e inteiro; nenhum encosta
|
|
5
|
+
// num tampo nem passa por cima de um colega sentado.
|
|
6
|
+
// Puro: sem DOM, sem relógio, sem rede.
|
|
7
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regra 28 (repositório do OpenCrew).
|
|
8
|
+
import { VAOS_X, mesaDe } from './modelo-mesas.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Índice (em `VAOS_X`) do vão por onde a entrega passa: o vizinho da coluna de origem, do lado
|
|
12
|
+
* do destino. Na mesma coluna, o da direita; na última coluna, o da esquerda.
|
|
13
|
+
*/
|
|
14
|
+
function vaoDe(origem, destino) {
|
|
15
|
+
if (destino > origem) return origem;
|
|
16
|
+
if (destino < origem) return origem - 1;
|
|
17
|
+
return origem < VAOS_X.length ? origem : origem - 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A rota entre duas mesas, pelo canto superior esquerdo do boneco.
|
|
22
|
+
* @param {number} de índice da mesa de quem entrega (0 a 11)
|
|
23
|
+
* @param {number} para índice da mesa de quem recebe (0 a 11)
|
|
24
|
+
* @returns {{ x: number, y: number }[] | null} quatro pontos: o assento de quem entrega, a
|
|
25
|
+
* entrada do vão, a saída do vão e a frente da mesa de quem recebe; `null` se uma das mesas
|
|
26
|
+
* não existe
|
|
27
|
+
*/
|
|
28
|
+
export function rota(de, para) {
|
|
29
|
+
const origem = mesaDe(de);
|
|
30
|
+
const destino = mesaDe(para);
|
|
31
|
+
if (!origem || !destino) return null;
|
|
32
|
+
const x = VAOS_X[vaoDe(origem.coluna, destino.coluna)];
|
|
33
|
+
return [{ ...origem.boneco }, { x, y: origem.boneco.y }, { x, y: destino.frente.y }, { ...destino.frente }];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const trechos = (pontos) => pontos.slice(1).map((fim, i) => [pontos[i], fim]);
|
|
37
|
+
const tamanho = ([a, b]) => Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
|
|
38
|
+
|
|
39
|
+
/** Comprimento de uma rota, em pixels lógicos. */
|
|
40
|
+
export function comprimentoDaRota(pontos) {
|
|
41
|
+
return trechos(pontos).reduce((soma, trecho) => soma + tamanho(trecho), 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Onde está quem já andou uma fração da rota, em velocidade constante.
|
|
46
|
+
* @param {{ x: number, y: number }[]} pontos a rota (ou ela invertida, para a volta)
|
|
47
|
+
* @param {number} fracao de 0 (o primeiro ponto) a 1 (o último); fora disso vale o extremo
|
|
48
|
+
* @returns {{ x: number, y: number, espelhado: boolean }} posição inteira · `espelhado`: o
|
|
49
|
+
* último trecho horizontal andado foi para a esquerda
|
|
50
|
+
*/
|
|
51
|
+
export function pontoDaRota(pontos, fracao) {
|
|
52
|
+
let resta = Math.round(comprimentoDaRota(pontos) * Math.max(0, fracao));
|
|
53
|
+
let espelhado = false;
|
|
54
|
+
for (const [a, b] of trechos(pontos)) {
|
|
55
|
+
if (a.x !== b.x) espelhado = b.x < a.x;
|
|
56
|
+
if (resta <= tamanho([a, b])) {
|
|
57
|
+
return { x: a.x + Math.sign(b.x - a.x) * resta, y: a.y + Math.sign(b.y - a.y) * resta, espelhado };
|
|
58
|
+
}
|
|
59
|
+
resta -= tamanho([a, b]);
|
|
60
|
+
}
|
|
61
|
+
return { ...pontos.at(-1), espelhado };
|
|
62
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Os rótulos sobre o desenho (regra 19): o nome de cada agente, sob a mesa dele, e o balão, sobre
|
|
2
|
+
// o boneco, são elementos da página numa camada por cima do canvas, escritos só por
|
|
3
|
+
// `textContent`. A posição vem das âncoras do modelo, em pixel lógico, e é aplicada em
|
|
4
|
+
// porcentagem de 320×180; cada rótulo tem a largura de uma coluna, e o CSS corta o texto que não
|
|
5
|
+
// cabe. A camada é `aria-hidden`: o leitor de tela lê a lista ao lado, que tem o texto inteiro.
|
|
6
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regras 18 e 19 (repositório do OpenCrew).
|
|
7
|
+
import { ALTURA, BONECO, LARGURA, LARGURA_COLUNA } from './modelo.js';
|
|
8
|
+
|
|
9
|
+
/** Altura, em pixels lógicos, da faixa que o nome ocupa sob a mesa. */
|
|
10
|
+
const ALTURA_DO_NOME = 10;
|
|
11
|
+
/** Quanto sobra do nome enquanto um boneco em pé está por cima dele. */
|
|
12
|
+
const QUASE_APAGADO = '0.15';
|
|
13
|
+
|
|
14
|
+
const porcento = (valor, total) => `${(valor / total) * 100}%`;
|
|
15
|
+
|
|
16
|
+
/** Um rótulo com a largura de uma coluna, centrado na âncora; o texto fica num `span`, que o CSS corta. */
|
|
17
|
+
function criarRotulo(documento, { classe, texto, ancora }) {
|
|
18
|
+
const caixa = documento.createElement('div');
|
|
19
|
+
const tinta = documento.createElement('span');
|
|
20
|
+
caixa.className = classe;
|
|
21
|
+
caixa.style.left = porcento(ancora.x - LARGURA_COLUNA / 2, LARGURA);
|
|
22
|
+
caixa.style.width = porcento(LARGURA_COLUNA, LARGURA);
|
|
23
|
+
tinta.textContent = texto;
|
|
24
|
+
caixa.append(tinta);
|
|
25
|
+
return caixa;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** O nome (sempre) e o balão (quando o modelo traz um) de um agente com mesa. */
|
|
29
|
+
function criarItem(documento, { mesa, nome, balao, acao }) {
|
|
30
|
+
const item = { mesa, nome: criarRotulo(documento, { classe: 'nome', texto: nome, ancora: mesa.nome }), balao: null };
|
|
31
|
+
item.nome.style.top = porcento(mesa.nome.y, ALTURA);
|
|
32
|
+
if (balao === null) return item;
|
|
33
|
+
item.balao = criarRotulo(documento, { classe: acao === 'checkpoint' ? 'balao espera' : 'balao', texto: balao, ancora: mesa.balao });
|
|
34
|
+
item.balao.style.bottom = porcento(ALTURA - mesa.balao.y, ALTURA);
|
|
35
|
+
return item;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** O boneco está sobre a faixa do nome desta mesa? */
|
|
39
|
+
function cobre(boneco, { nome }) {
|
|
40
|
+
const esquerda = nome.x - LARGURA_COLUNA / 2;
|
|
41
|
+
const naColuna = boneco.x < esquerda + LARGURA_COLUNA && boneco.x + BONECO > esquerda;
|
|
42
|
+
return naColuna && boneco.y < nome.y + ALTURA_DO_NOME && boneco.y + BONECO > nome.y;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function opacidade(elemento, valor) {
|
|
46
|
+
if (elemento.style.opacity !== valor) elemento.style.opacity = valor;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A camada de nomes e balões.
|
|
51
|
+
* @param {object} camada o elemento que fica sobre o canvas, do mesmo tamanho dele
|
|
52
|
+
* @param {object} documento de quem vem o `createElement`
|
|
53
|
+
* @returns {{ atualizar: (agentes: object[]) => void, acompanhar: (bonecos: object[]) => void }}
|
|
54
|
+
* `atualizar`: recebe os agentes do modelo a cada leitura e só refaz a camada quando um nome,
|
|
55
|
+
* um balão ou uma mesa mudou · `acompanhar`: recebe os bonecos de cada quadro; o nome sob um
|
|
56
|
+
* boneco em pé quase some, para não cobrir quem entrega, e o balão de quem saiu da mesa some
|
|
57
|
+
* até ele voltar
|
|
58
|
+
*/
|
|
59
|
+
export function criarRotulos(camada, documento) {
|
|
60
|
+
let itens = [];
|
|
61
|
+
let escrito = '';
|
|
62
|
+
function atualizar(agentes) {
|
|
63
|
+
const comMesa = agentes.filter((agente) => agente.mesa);
|
|
64
|
+
const novo = JSON.stringify(comMesa.map((a) => [a.mesa.indice, a.nome, a.balao, a.acao]));
|
|
65
|
+
if (novo === escrito) return;
|
|
66
|
+
escrito = novo;
|
|
67
|
+
itens = comMesa.map((agente) => criarItem(documento, agente));
|
|
68
|
+
camada.replaceChildren(...itens.flatMap((item) => (item.balao ? [item.nome, item.balao] : [item.nome])));
|
|
69
|
+
}
|
|
70
|
+
function acompanhar(bonecos) {
|
|
71
|
+
const emPe = bonecos.filter((boneco) => !boneco.sentado);
|
|
72
|
+
for (const { mesa, nome, balao } of itens) {
|
|
73
|
+
opacidade(nome, emPe.some((boneco) => cobre(boneco, mesa)) ? QUASE_APAGADO : '');
|
|
74
|
+
if (balao) opacidade(balao, emPe.some((boneco) => boneco.indice === mesa.indice) ? '0' : '');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { atualizar, acompanhar };
|
|
78
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// A mesa de cada agente e o que muda nela, em matrizes de pixels: o tampo com os pés, o monitor
|
|
2
|
+
// com as telas de cada status e os sinais que aparecem sobre a cabeça do boneco (✓, "!" e "?"
|
|
3
|
+
// são desenhos, não texto). Quem diz onde cada desenho vai é `cena.js`.
|
|
4
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regras 16, 18 e 19 (repositório do OpenCrew).
|
|
5
|
+
import { CORES, matriz } from './sprites.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A mesa vista de cima e de frente: o tampo (com teclado, papel e caneca), a borda, a saia com a
|
|
9
|
+
* gaveta e os pés. Tem a largura do retângulo da mesa no modelo e `MESA_ACIMA` linhas a mais.
|
|
10
|
+
*/
|
|
11
|
+
export const MESA = matriz({
|
|
12
|
+
o: CORES.tinta, t: CORES.madeiraLuz, T: CORES.madeira, F: CORES.madeiraMedia, D: CORES.madeiraEscura, x: CORES.junta,
|
|
13
|
+
k: CORES.cinzaEscuro, K: CORES.cinzaClaro, w: CORES.branco, g: CORES.cinzaClaro, c: CORES.ceu, f: CORES.madeiraEscura,
|
|
14
|
+
}, [
|
|
15
|
+
'.oooooooooooooooooooooooooooooooooooooooooo.',
|
|
16
|
+
'otttttttttttttttkkkkkkkkkkkkttttttttttttttto',
|
|
17
|
+
'oTTTTTTTTTTTTTTTkKKKKKKKKKKkTTTTwwwwwwTcccTo',
|
|
18
|
+
'oTTTTTTTTTTTTTTTkkkkkkkkkkkkTTTTwggggwTcfcTo',
|
|
19
|
+
'oTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTwwwwwwTcccTo',
|
|
20
|
+
'oTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTwgggwwTTTTTo',
|
|
21
|
+
'oTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTwwwwwwTTTTTo',
|
|
22
|
+
'oFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFo',
|
|
23
|
+
'oooooooooooooooooooooooooooooooooooooooooooo',
|
|
24
|
+
'.oDDDDDDDDDDDDDDDDDDFFFFDDDDDDDDDDDDDDDDDDo.',
|
|
25
|
+
'.oDDooooooooooooooooooooooooooooooooooooDDo.',
|
|
26
|
+
'.oDDoxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxoDDo.',
|
|
27
|
+
'.oDDoxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxoDDo.',
|
|
28
|
+
'.oDDo..................................oDDo.',
|
|
29
|
+
'.oooo..................................oooo.',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Quantas linhas do desenho da mesa ficam acima do retângulo do tampo: é o fundo do tampo, que
|
|
34
|
+
* cobre as pernas de quem está sentado atrás dele.
|
|
35
|
+
*/
|
|
36
|
+
export const MESA_ACIMA = 4;
|
|
37
|
+
|
|
38
|
+
/** O monitor, de frente para quem olha, com a tela apagada; a base pousa no fundo do tampo. */
|
|
39
|
+
export const MONITOR = matriz({ o: CORES.tinta, g: CORES.cinzaEscuro, k: CORES.telaApagada, G: CORES.cinzaClaro }, [
|
|
40
|
+
'oooooooooooo',
|
|
41
|
+
'oggggggggggo',
|
|
42
|
+
'ogkkkkkkkkgo',
|
|
43
|
+
'ogkkkkkkkkgo',
|
|
44
|
+
'ogkkkkkkkkgo',
|
|
45
|
+
'ogkkkkkkkkgo',
|
|
46
|
+
'ogkkkkkkkkgo',
|
|
47
|
+
'oggggggggggo',
|
|
48
|
+
'oooooooooooo',
|
|
49
|
+
'....oGGo....',
|
|
50
|
+
'..oGGGGGGo..',
|
|
51
|
+
'..oooooooo..',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** Onde o monitor fica, a partir do canto do retângulo da mesa; e a tela, a partir do canto do monitor. */
|
|
55
|
+
export const MONITOR_EM = Object.freeze({ x: 1, y: -13 });
|
|
56
|
+
export const TELA_EM = Object.freeze({ x: 2, y: 2 });
|
|
57
|
+
|
|
58
|
+
const CODIGO = ['vlllllvv', 'vvvvvvvv', 'vlllvllv', 'vvvvvvvv'];
|
|
59
|
+
const LINHAS_DA_TELA = [0, 1, 2, 3, 4];
|
|
60
|
+
|
|
61
|
+
/** O monitor aceso, uma tela por fase (0 a 3): as linhas de código sobem um pixel a cada fase. */
|
|
62
|
+
export const TELAS_ACESAS = Object.freeze([0, 1, 2, 3].map((fase) => matriz(
|
|
63
|
+
{ v: CORES.telaAcesa, l: CORES.verde },
|
|
64
|
+
LINHAS_DA_TELA.map((linha) => CODIGO[(linha + fase) % CODIGO.length]),
|
|
65
|
+
)));
|
|
66
|
+
|
|
67
|
+
/** Âmbar, com o sinal de pausa: está esperando o usuário. */
|
|
68
|
+
export const TELA_AMBAR = matriz({ y: CORES.ambar, o: CORES.tinta }, [
|
|
69
|
+
'yyyyyyyy',
|
|
70
|
+
'yyoyyoyy',
|
|
71
|
+
'yyoyyoyy',
|
|
72
|
+
'yyoyyoyy',
|
|
73
|
+
'yyyyyyyy',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/** Vermelha, com um xis: falhou. */
|
|
77
|
+
export const TELA_VERMELHA = matriz({ r: CORES.vermelho, o: CORES.tinta }, [
|
|
78
|
+
'rrrrrrrr',
|
|
79
|
+
'rrorrorr',
|
|
80
|
+
'rrroorrr',
|
|
81
|
+
'rrorrorr',
|
|
82
|
+
'rrrrrrrr',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
/** Sobre a cabeça de quem concluiu. */
|
|
86
|
+
export const SINAL_FEITO = matriz({ o: CORES.tinta, g: CORES.verde }, [
|
|
87
|
+
'......ooo',
|
|
88
|
+
'.....oogo',
|
|
89
|
+
'ooo.ooggo',
|
|
90
|
+
'ogoooggoo',
|
|
91
|
+
'oggoggoo.',
|
|
92
|
+
'oogggoo..',
|
|
93
|
+
'.oogoo...',
|
|
94
|
+
'..ooo....',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/** Sobre a cabeça de quem falhou. */
|
|
98
|
+
export const SINAL_FALHA = matriz({ o: CORES.tinta, r: CORES.vermelho }, [
|
|
99
|
+
'oooo',
|
|
100
|
+
'orro',
|
|
101
|
+
'orro',
|
|
102
|
+
'orro',
|
|
103
|
+
'orro',
|
|
104
|
+
'oooo',
|
|
105
|
+
'orro',
|
|
106
|
+
'oooo',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
/** Sobre a cabeça de quem trabalhava e está sem sinal. */
|
|
110
|
+
export const SINAL_DUVIDA = matriz({ o: CORES.tinta, w: CORES.branco }, [
|
|
111
|
+
'.oooo.',
|
|
112
|
+
'oowwoo',
|
|
113
|
+
'owoowo',
|
|
114
|
+
'ooowoo',
|
|
115
|
+
'.owoo.',
|
|
116
|
+
'.ooo..',
|
|
117
|
+
'.owo..',
|
|
118
|
+
'.ooo..',
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
/** A sombra no chão de quem está em pé, fora da mesa. */
|
|
122
|
+
export const SOMBRA = matriz({ x: CORES.junta }, ['.xxxxxxxx.']);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// A sala do escritório, em matrizes de pixels: a parede do fundo, o piso em ladrilho de dois tons
|
|
2
|
+
// e o que enfeita a parede (janela, relógio, planta). Quem diz onde cada desenho vai é `cena.js`.
|
|
3
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regra 16 (repositório do OpenCrew).
|
|
4
|
+
import { CORES, matriz } from './sprites.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A parede, numa faixa de 8 px que se repete de lado a lado: sanca de madeira, papel de parede
|
|
8
|
+
* listrado e lambri. A altura é a da parede no modelo (`TOPO`).
|
|
9
|
+
*/
|
|
10
|
+
export const PAREDE = matriz({ m: CORES.madeiraMedia, d: CORES.madeiraEscura, p: CORES.parede, P: CORES.paredeSombra, h: CORES.madeiraLuz }, [
|
|
11
|
+
'mmmmmmmm',
|
|
12
|
+
'dddddddd',
|
|
13
|
+
'PPPPPPPP',
|
|
14
|
+
'pppppppP',
|
|
15
|
+
'pppppppP',
|
|
16
|
+
'pppppppP',
|
|
17
|
+
'pppppppP',
|
|
18
|
+
'pppppppP',
|
|
19
|
+
'pppppppP',
|
|
20
|
+
'pppppppP',
|
|
21
|
+
'pppppppP',
|
|
22
|
+
'pppppppP',
|
|
23
|
+
'pppppppP',
|
|
24
|
+
'pppppppP',
|
|
25
|
+
'hhhhhhhh',
|
|
26
|
+
'mmmmmmmm',
|
|
27
|
+
'mmmmmmmm',
|
|
28
|
+
'dddddddd',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const LADRILHO_LINHAS = Object.freeze([
|
|
32
|
+
'jjjjjjjjjjjjjjjj',
|
|
33
|
+
'jaaaaaaaaaaaaaaa',
|
|
34
|
+
'jaaaaaaaaaaaaaaa',
|
|
35
|
+
'jaaaaaaaaaaaaaaa',
|
|
36
|
+
'jaaaaaaaaaaaaaaa',
|
|
37
|
+
'jaaaaaaaaaaaaaaa',
|
|
38
|
+
'jaaaaaaaaaaaaaaa',
|
|
39
|
+
'jaaaaaaaaaaaaaaa',
|
|
40
|
+
'jaaaaaaaaaaaaaaa',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/** O ladrilho do piso, com a junta em cima e à esquerda; o claro e o escuro se alternam em xadrez. */
|
|
44
|
+
export const LADRILHO = matriz({ j: CORES.junta, a: CORES.pisoClaro }, LADRILHO_LINHAS);
|
|
45
|
+
export const LADRILHO_ESCURO = matriz({ j: CORES.junta, a: CORES.pisoEscuro }, LADRILHO_LINHAS);
|
|
46
|
+
|
|
47
|
+
/** Janela de duas folhas: um reflexo no vidro da esquerda, uma nuvem no da direita. */
|
|
48
|
+
export const JANELA = matriz({ o: CORES.tinta, w: CORES.branco, b: CORES.ceu, B: CORES.ceuClaro }, [
|
|
49
|
+
'oooooooooooooooooooooooo',
|
|
50
|
+
'owwwwwwwwwwwwwwwwwwwwwwo',
|
|
51
|
+
'owbbbbbBBbbwwbbbbbbbbbwo',
|
|
52
|
+
'owbbbbBBbbbwwbbbbwwbbbwo',
|
|
53
|
+
'owbbbBBbbbbwwbbwwwwwbbwo',
|
|
54
|
+
'owbbBBbbbbbwwbbbbbbbbbwo',
|
|
55
|
+
'owbBBbbbbbbwwbbbbbbbbbwo',
|
|
56
|
+
'owbbbbbbbbbwwbbbbbbbbbwo',
|
|
57
|
+
'owwwwwwwwwwwwwwwwwwwwwwo',
|
|
58
|
+
'oooooooooooooooooooooooo',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/** Relógio de parede, parado nas três horas: nada na sala se mexe sem um agente trabalhando. */
|
|
62
|
+
export const RELOGIO = matriz({ o: CORES.tinta, w: CORES.branco }, [
|
|
63
|
+
'..ooooo..',
|
|
64
|
+
'.owwwwwo.',
|
|
65
|
+
'owwwowwwo',
|
|
66
|
+
'owwwowwwo',
|
|
67
|
+
'owwwooowo',
|
|
68
|
+
'owwwwwwwo',
|
|
69
|
+
'owwwwwwwo',
|
|
70
|
+
'.owwwwwo.',
|
|
71
|
+
'..ooooo..',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
/** Planta em vaso de barro, para o chão junto à parede. */
|
|
75
|
+
export const PLANTA = matriz({ o: CORES.tinta, g: CORES.folha, G: CORES.folhaEscura, t: CORES.madeiraEscura, v: CORES.madeiraMedia, V: CORES.madeiraEscura }, [
|
|
76
|
+
'...ooooo...',
|
|
77
|
+
'..oggggGo..',
|
|
78
|
+
'.oggGggggo.',
|
|
79
|
+
'oggggggGggo',
|
|
80
|
+
'ogGggggggGo',
|
|
81
|
+
'oggggGggggo',
|
|
82
|
+
'.ogGgggGgo.',
|
|
83
|
+
'..oggggGo..',
|
|
84
|
+
'...oGGGo...',
|
|
85
|
+
'....oto....',
|
|
86
|
+
'..ooooooo..',
|
|
87
|
+
'..ovvvvvo..',
|
|
88
|
+
'..ovvvvVo..',
|
|
89
|
+
'...ovvVo...',
|
|
90
|
+
'...ovvVo...',
|
|
91
|
+
'...ooooo...',
|
|
92
|
+
]);
|