primocode 10.5.0 → 10.6.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.
@@ -0,0 +1,305 @@
1
+ /**
2
+ * mundo.js — o lugar, os bichos e as coisas do vídeo 3D, a partir do PEDIDO.
3
+ *
4
+ * "não quero que ele só troque as falas e os textos ... se eu pedir dois
5
+ * humanos ele vai fazer dois humanos, se eu pedir um cachorro ele vai fazer,
6
+ * se eu pedir um humano na praia ele vai fazer — vai mudar o cenário, vai
7
+ * mudar as coisas, e não é coisa pré-definida" (Joel, 11/10)
8
+ *
9
+ * Até a 10.5 o 3D tinha UM set (a parede de telas) e o elenco era gente ou o
10
+ * mascote. Aqui o set vira um MUNDO montado de peças:
11
+ *
12
+ * · o LUGAR (praia, cidade, floresta, espaço, cozinha…): céu, chão, água,
13
+ * luz e hora do dia; vem do texto livre ("numa praia ao pôr do sol");
14
+ * · os ELEMENTOS: o que há no lugar. Uma palavra do catálogo (palmeira, carro,
15
+ * prédio, foguete…) vira uma peça de blocos desenhada no kit/mundo.jsx; uma
16
+ * palavra FORA do catálogo vira um ícone 3D da biblioteca (colecao.js,
17
+ * 1.595 objetos) transformado em blocos na hora do render — é isso que
18
+ * tira o "pré-definido": qualquer coisa que tenha ícone entra na cena;
19
+ * · o ELENCO: pessoa, robô/mascote, BICHO (cachorro, gato, cavalo, vaca,
20
+ * pinguim, peixe, dinossauro… — o kit/mundo.jsx monta cada espécie) ou
21
+ * COISA (um ícone em blocos que fala: "uma pizza conversando com um café").
22
+ * E os FIGURANTES: quem está na cena sem falar ("um humano com o cachorro").
23
+ *
24
+ * A IA decide (roteiro.cena, roteiro.personagens, parte.cenario); o que ela
25
+ * não disse sai do texto do pedido e das falas — a mesma regra do resto do
26
+ * Estúdio: o modelo manda, a leitura do texto só completa. É o que faz um
27
+ * modelo local pequeno, que só escreve as falas, ainda ganhar a praia que a
28
+ * pessoa pediu.
29
+ */
30
+
31
+ 'use strict';
32
+
33
+ const sem = (s) => String(s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
34
+
35
+ /* ── OS LUGARES ─────────────────────────────────────────────────────────
36
+ ceu: [alto, horizonte] · chao: {tipo, cor, cor2} · agua: cor do mar ·
37
+ dentro: set fechado (parede no fundo) · props: o que o lugar tem quando a
38
+ cena não disse. As palavras são lidas sem acento. */
39
+ const LUGARES = {
40
+ /* A COBERTURA À NOITE (2ª referência do Joel, 11/10): a janela de vidro
41
+ com a cidade acesa lá fora, as luzes grandes desfocadas (bokeh), o
42
+ parapeito escuro e os CARTÕES de interface flutuando na frente (o
43
+ assunto do vídeo, desfocados pela lente). É o set padrão quando o
44
+ pedido não diz lugar. */
45
+ cobertura: { palavras: /\b(cobertura|terraco|rooftop|janela|vista da cidade|apartamento alto|predio alto|escritorio a noite)\b/, dentro: true, cobertura: true,
46
+ parede: '#050816', chao: { tipo: 'madeira', cor: '#2a1f1a', cor2: '#241a16' }, luz: '#ffd2b0', recorte: '#6f8bff', acento: '#ff6a2b', props: [] },
47
+ telas: { palavras: /\b(estudio|podcast|tecnologia|telas?|set|programa|live|transmissao)\b/, dentro: true, telas: true,
48
+ parede: '#07061a', chao: { tipo: 'ladrilho', cor: '#4338ca', cor2: '#3b32b8' }, luz: '#ffd2b0', recorte: '#8b7bff', acento: '#ff7a45', props: [] },
49
+ praia: { palavras: /\b(praia|mar|litoral|beira[ -]mar|areia|orla|surf\w*|ilha)\b/, ceu: ['#3d8fe0', '#c4e8ff'], agua: '#1d8bc9',
50
+ chao: { tipo: 'areia', cor: '#ead29c', cor2: '#dfc48a' }, luz: '#fff2d6', recorte: '#ffd08a', acento: '#ff7a45', sol: true,
51
+ props: ['palmeira', 'palmeira', 'guarda-sol', 'cadeira de praia', 'palmeira', 'castelo de areia', 'pedra'] },
52
+ cidade: { palavras: /\b(cidade|rua|avenida|centro|predios?|esquina|calcada|metropole|urbano)\b/, ceu: ['#5b9be6', '#dbeaff'],
53
+ chao: { tipo: 'asfalto', cor: '#5b5e67', cor2: '#53565f' }, luz: '#fff1dc', recorte: '#9fc3ff', acento: '#facc15', sol: true,
54
+ props: ['predio', 'predio', 'predio', 'poste', 'carro', 'arvore', 'predio', 'poste', 'carro', 'predio'] },
55
+ floresta: { palavras: /\b(floresta|mata|selva|bosque|trilha|acampamento|amazonia)\b/, ceu: ['#6fb3e8', '#dcf2d6'],
56
+ chao: { tipo: 'grama', cor: '#4f9a3c', cor2: '#468c35' }, luz: '#fff0d0', recorte: '#b6f0a0', acento: '#facc15', sol: true, neblina: 0.6,
57
+ props: ['arvore', 'pinheiro', 'arvore', 'pinheiro', 'arbusto', 'pedra', 'arvore', 'pinheiro', 'cogumelo', 'arvore'] },
58
+ parque: { palavras: /\b(parque|jardim|praca|gramado|piquenique)\b/, ceu: ['#58a6ee', '#d9eeff'],
59
+ chao: { tipo: 'grama', cor: '#5aa845', cor2: '#4f9a3c' }, luz: '#fff2d6', recorte: '#c8f0b0', acento: '#f472b6', sol: true,
60
+ props: ['arvore', 'banco', 'flor', 'arvore', 'poste', 'flor', 'arbusto', 'arvore', 'flor'] },
61
+ fazenda: { palavras: /\b(fazenda|sitio|roca|interior|celeiro|curral|plantacao|colheita)\b/, ceu: ['#68aef0', '#f3ecd0'],
62
+ chao: { tipo: 'grama', cor: '#6aa84f', cor2: '#5e9a44' }, luz: '#fff1d0', recorte: '#ffe0a0', acento: '#dc2626', sol: true,
63
+ props: ['celeiro', 'cerca', 'cerca', 'arvore', 'feno', 'cerca', 'arvore', 'feno'] },
64
+ neve: { palavras: /\b(neve|inverno|gelo|polo|nevando|esqui|frio)\b/, ceu: ['#9fbfe6', '#eef4fb'],
65
+ chao: { tipo: 'neve', cor: '#f4f8fc', cor2: '#e6eef6' }, luz: '#eef4ff', recorte: '#a8c8ff', acento: '#38bdf8', sol: true,
66
+ props: ['pinheiro nevado', 'pinheiro nevado', 'boneco de neve', 'montanha', 'pinheiro nevado', 'pedra', 'pinheiro nevado'] },
67
+ deserto: { palavras: /\b(deserto|duna|sertao|saara|oasis)\b/, ceu: ['#f0a35a', '#ffe2ae'],
68
+ chao: { tipo: 'areia', cor: '#e2b06c', cor2: '#d6a05c' }, luz: '#ffe6c0', recorte: '#ffb070', acento: '#ea580c', sol: true,
69
+ props: ['cacto', 'cacto', 'pedra', 'montanha', 'cacto', 'pedra'] },
70
+ espaco: { palavras: /\b(espaco|lua|planeta|galaxia|foguete|astronauta|marte|orbita|estrelas|universo|nave)\b/, ceu: ['#02020f', '#0d0b2c'], estrelas: true,
71
+ chao: { tipo: 'lua', cor: '#a1a1ad', cor2: '#90909c' }, luz: '#e8ecff', recorte: '#8b7bff', acento: '#a78bfa',
72
+ props: ['planeta', 'foguete', 'pedra', 'planeta', 'cratera', 'pedra'] },
73
+ fundo_do_mar: { palavras: /\b(fundo do mar|submarino|oceano|aquario|mergulh\w*|recife|coral)\b/, ceu: ['#022c46', '#0b6f94'], subaquatico: true,
74
+ chao: { tipo: 'areia', cor: '#c9b98a', cor2: '#bba97a' }, luz: '#bfefff', recorte: '#38bdf8', acento: '#f97316', neblina: 1,
75
+ props: ['alga', 'coral', 'pedra', 'alga', 'coral', 'bau', 'alga'] },
76
+ escritorio: { palavras: /\b(escritorio|trabalho|reuniao|empresa|firma|coworking|startup)\b/, dentro: true, parede: '#d8d0c2',
77
+ chao: { tipo: 'madeira', cor: '#9a7350', cor2: '#8d6846' }, luz: '#fff1dc', recorte: '#ffe2b8', acento: '#2563eb',
78
+ props: ['mesa', 'computador', 'planta', 'estante', 'cadeira', 'quadro'] },
79
+ sala: { palavras: /\b(sala|casa|sofa|quarto|lar|apartamento|sala de estar)\b/, dentro: true, parede: '#e6d6c3',
80
+ chao: { tipo: 'madeira', cor: '#a87b52', cor2: '#9a6e47' }, luz: '#ffe8c8', recorte: '#ffd6a0', acento: '#e11d48',
81
+ props: ['sofa', 'luminaria', 'planta', 'estante', 'tapete', 'quadro'] },
82
+ cozinha: { palavras: /\b(cozinha|restaurante|padaria|cafeteria|cafe|lanchonete|confeitaria|pizzaria|bar)\b/, dentro: true, parede: '#efe4d2',
83
+ chao: { tipo: 'ladrilho', cor: '#e8e1d4', cor2: '#cfc5b4' }, luz: '#fff0d6', recorte: '#ffd9a0', acento: '#dc2626',
84
+ props: ['balcao', 'mesa', 'cadeira', 'planta', 'estante', 'luminaria'] },
85
+ escola: { palavras: /\b(escola|sala de aula|aula|professor|faculdade|universidade|colegio)\b/, dentro: true, parede: '#dfe8dc',
86
+ chao: { tipo: 'madeira', cor: '#b08458', cor2: '#a3794f' }, luz: '#fffaf0', recorte: '#d6f0ff', acento: '#16a34a',
87
+ props: ['lousa', 'mesa', 'mesa', 'estante', 'planta'] },
88
+ palco: { palavras: /\b(palco|show|teatro|concerto|festa|balada|evento)\b/, dentro: true, parede: '#0b0612',
89
+ chao: { tipo: 'madeira', cor: '#3a2a20', cor2: '#33251c' }, luz: '#ffe1f5', recorte: '#ff4fd8', acento: '#22d3ee', holofotes: true,
90
+ props: ['caixa de som', 'caixa de som', 'luminaria'] },
91
+ campo: { palavras: /\b(campo|montanhas?|colina|natureza|ao ar livre|vale)\b/, ceu: ['#5aa3ec', '#dcefff'],
92
+ chao: { tipo: 'grama', cor: '#5ca846', cor2: '#529b3e' }, luz: '#fff2d6', recorte: '#c8f0b0', acento: '#facc15', sol: true,
93
+ props: ['montanha', 'arvore', 'flor', 'arvore', 'pedra', 'montanha', 'flor'] },
94
+ };
95
+ // O que o set antigo chamava por outro nome.
96
+ const APELIDOS_LUGAR = { neon: 'palco', oceano: 'fundo_do_mar', terra: 'deserto', podcast: 'telas', estudio: 'telas', noite: 'telas' };
97
+
98
+ const HORAS = {
99
+ noite: { palavras: /\b(noite|noturn\w*|madrugada|luar|a noite|de noite)\b/, ceu: ['#070b24', '#1c2552'], luz: '#b8c6ff', forca: 0.45, lua: true, estrelas: true },
100
+ por_do_sol: { palavras: /\b(por do sol|entardecer|fim de tarde|crepusculo|poente|anoitecer)\b/, ceu: ['#46368a', '#ff9a5a'], luz: '#ffb27a', forca: 0.8, solBaixo: true },
101
+ amanhecer: { palavras: /\b(amanhecer|nascer do sol|alvorada|de manha cedo)\b/, ceu: ['#6a8fd8', '#ffd2a8'], luz: '#ffd9b0', forca: 0.85, solBaixo: true },
102
+ };
103
+
104
+ /* ── O CATÁLOGO DE PEÇAS (kit/mundo.jsx desenha cada uma) ───────────────
105
+ A chave é a peça; as palavras são como a pessoa (ou a IA) diz. */
106
+ const PECAS = {
107
+ palmeira: /\bpalmeiras?|coqueiros?\b/, arvore: /\barvores?|arvorezinha\b/, pinheiro: /\bpinheiros?\b/, 'pinheiro nevado': /\bpinheiros? nevados?\b/,
108
+ arbusto: /\barbustos?|moitas?\b/, flor: /\bflor(es)?\b/, pedra: /\bpedras?|rochas?\b/, cacto: /\bcactos?\b/, montanha: /\bmontanhas?|morros?\b/,
109
+ casa: /\bcasas?|casinha\b/, predio: /\bpredios?|edificios?|arranha[- ]ceus?\b/, celeiro: /\bceleiros?\b/, poste: /\bpostes?\b/,
110
+ carro: /\bcarros?|automove(l|is)\b/, 'guarda-sol': /\bguarda[- ]sois?|guarda[- ]sol\b/, 'cadeira de praia': /\bcadeiras? de praia\b/,
111
+ 'castelo de areia': /\bcastelos? de areia\b/, mesa: /\bmesas?\b/, cadeira: /\bcadeiras?\b/, sofa: /\bsofas?\b/, planta: /\bplantas?|vasos?\b/,
112
+ estante: /\bestantes?|prateleiras?\b/, computador: /\bcomputador(es)?|notebooks?|pc\b/, luminaria: /\bluminarias?|abajur\b/,
113
+ quadro: /\bquadros?\b/, lousa: /\blousas?|quadro negro\b/, balcao: /\bbalca(o|oes)\b/, tapete: /\btapetes?\b/, banco: /\bbancos? de praca|banquinhos?\b/,
114
+ cerca: /\bcercas?\b/, feno: /\bfenos?\b/, 'boneco de neve': /\bbonecos? de neve\b/, foguete: /\bfoguetes?\b/, planeta: /\bplanetas?\b/,
115
+ cratera: /\bcrateras?\b/, alga: /\balgas?\b/, coral: /\bcorais?|coral\b/, bau: /\bbaus?|tesouros?\b/, cogumelo: /\bcogumelos?\b/,
116
+ nuvem: /\bnuvens?\b/, fogueira: /\bfogueiras?\b/, barraca: /\bbarracas?|tendas?\b/, barco: /\bbarcos?|canoas?\b/,
117
+ 'caixa de som': /\bcaixas? de som|alto[- ]falantes?\b/,
118
+ };
119
+
120
+ /* ── AS ESPÉCIES (kit/mundo.jsx monta cada uma de blocos) ────────────── */
121
+ const ESPECIES = {
122
+ cachorro: /\b(cachorr\w*|cao|caes|cachorrinho|dog|vira[- ]lata|labrador|filhote)\b/, gato: /\b(gat[oa]s?|gatinh\w*|felino)\b/,
123
+ cavalo: /\b(caval\w*|egua|potro|pônei|ponei)\b/, vaca: /\b(vacas?|boi|bois|touro|bezerro)\b/, porco: /\b(porc\w*|leitao)\b/,
124
+ ovelha: /\b(ovelhas?|carneiros?|cordeiro)\b/, raposa: /\b(raposas?)\b/, lobo: /\b(lob[oa]s?)\b/, urso: /\b(urs[oa]s?)\b/,
125
+ panda: /\b(pandas?)\b/, coelho: /\b(coelh\w*)\b/, leao: /\b(leao|leoes|leoa)\b/, tigre: /\b(tigres?)\b/, elefante: /\b(elefantes?)\b/,
126
+ girafa: /\b(girafas?)\b/, galinha: /\b(galinhas?|galos?|pintinhos?)\b/, pato: /\b(pat[oa]s?|patinho)\b/, pinguim: /\b(pinguins?)\b/,
127
+ passaro: /\b(passar\w*|ave|aves|passarinho)\b/, papagaio: /\b(papagaios?|arara)\b/, coruja: /\b(corujas?)\b/,
128
+ peixe: /\b(peixes?|peixinho)\b/, tubarao: /\b(tubara(o|oes))\b/, golfinho: /\b(golfinhos?)\b/, tartaruga: /\b(tartarugas?|jabuti)\b/,
129
+ dinossauro: /\b(dinossauros?|dino|t[- ]?rex)\b/, sapo: /\b(sapos?|ra)\b/, macaco: /\b(macac\w*)\b/,
130
+ };
131
+
132
+ const PESSOA = /\b(humanos?|pessoas?|homens?|mulher(es)?|menin[oa]s?|crianc\w*|amig[oa]s?|garot[oa]s?|rapaz|mo[cç][oa]s?|senhor(a|es)?|vov[oóô]|av[oóô]s?|casal|familia|cliente|vendedor\w*|professor\w*|alun[oa]s?|medic[oa]s?|astronautas?|surfistas?|chef|cozinheir[oa]s?)\b/;
133
+ const ROBO = /\b(robo|robos|ia|inteligencia artificial|mascote|claude|chatgpt|gpt|gemini|assistente|bot)\b/;
134
+ const NUMEROS = { um: 1, uma: 1, dois: 2, duas: 2, tres: 3, quatro: 4, cinco: 5, casal: 2, par: 2 };
135
+
136
+ function contar(texto, re) {
137
+ // "dois humanos", "3 cachorros", "um casal": o número logo antes da palavra.
138
+ const s = sem(texto);
139
+ if (re === PESSOA && /\b(um )?casal\b/.test(s)) return 2;
140
+ const m = new RegExp(`(\\d+|um|uma|dois|duas|tres|quatro|cinco)\\s+(?:\\w+\\s+)?${re.source.replace(/^\\b|\\b$/g, '')}`).exec(s);
141
+ if (m) return Math.min(4, Number(m[1]) || NUMEROS[m[1]] || 1);
142
+ return re.test(s) ? 1 : 0;
143
+ }
144
+
145
+ /** O lugar e a hora que o texto pede, ou null. */
146
+ function lerLugar(texto) {
147
+ const s = sem(texto);
148
+ let lugar = null, onde = -1;
149
+ // O mais específico primeiro: "fundo do mar" antes de "mar", "sala de aula" antes de "sala".
150
+ const ordem = ['cobertura', 'fundo_do_mar', 'escola', 'praia', 'espaco', 'neve', 'deserto', 'fazenda', 'floresta', 'parque', 'cidade', 'cozinha', 'escritorio', 'palco', 'sala', 'campo', 'telas'];
151
+ for (const n of ordem) { const m = LUGARES[n].palavras.exec(s); if (m) { lugar = n; onde = m.index; break; } }
152
+ /* A hora vale para AQUELE lugar: "na praia, depois na cidade à noite" é
153
+ praia de dia. Com lugar no texto, só a hora a até 40 letras dele. */
154
+ let hora = null;
155
+ for (const [n, h] of Object.entries(HORAS)) {
156
+ const m = h.palavras.exec(s);
157
+ if (!m) continue;
158
+ if (onde < 0) { hora = n; break; }
159
+ // Outro lugar no meio ("praia, depois vão pra cidade à noite"): a hora é DELE.
160
+ const entre = s.slice(Math.min(m.index, onde) + 1, Math.max(m.index, onde));
161
+ const outroLugar = Object.entries(LUGARES).some(([k, L]) => k !== lugar && L.palavras.test(entre));
162
+ if (!outroLugar && Math.abs(m.index - onde) <= 40) { hora = n; break; }
163
+ }
164
+ return { lugar, hora };
165
+ }
166
+
167
+ /** As peças do catálogo e os bichos que o texto cita (em ordem de aparição). */
168
+ function lerCoisas(texto) {
169
+ const s = sem(texto);
170
+ const pecas = [];
171
+ for (const [nome, re] of Object.entries(PECAS)) {
172
+ const m = re.exec(s);
173
+ if (m) pecas.push({ nome, em: m.index });
174
+ }
175
+ // "pinheiro nevado" ganha de "pinheiro"; "cadeira de praia" ganha de "cadeira".
176
+ const filtradas = pecas.filter((p) => !pecas.some((q) => q !== p && q.nome.startsWith(p.nome + ' ') && Math.abs(q.em - p.em) < 3)
177
+ && !pecas.some((q) => q !== p && q.nome.includes(p.nome) && q.nome !== p.nome && Math.abs(q.em - p.em) < 12));
178
+ const bichos = [];
179
+ for (const [esp, re] of Object.entries(ESPECIES)) {
180
+ const m = re.exec(s);
181
+ if (m) bichos.push({ especie: esp, em: m.index, quantos: contar(texto, re) || 1 });
182
+ }
183
+ return { pecas: filtradas.sort((a, b) => a.em - b.em).map((p) => p.nome), bichos: bichos.sort((a, b) => a.em - b.em) };
184
+ }
185
+
186
+ /** O que o PEDIDO descreve do elenco: quantas pessoas, que bichos, se há IA. */
187
+ function lerElenco(texto) {
188
+ const s = sem(texto);
189
+ const { bichos } = lerCoisas(texto);
190
+ return { pessoas: contar(texto, PESSOA), robo: ROBO.test(s), bichos };
191
+ }
192
+
193
+ /* ── O MUNDO MONTADO ────────────────────────────────────────────────────
194
+ A peça é posta numa faixa ATRÁS e DOS LADOS do elenco, nunca no corredor
195
+ entre a câmera e quem fala. Com mar, a faixa é a areia antes da água. */
196
+ function rngDe(semente) {
197
+ let s = (Number(semente) >>> 0) || 1;
198
+ return () => { s = (Math.imul(s ^ (s >>> 15), 2246822507) + 0x9e3779b9) >>> 0; return s / 4294967296; };
199
+ }
200
+
201
+ const ehCor = (c) => typeof c === 'string' && /^#[0-9a-f]{6}$/i.test(c);
202
+
203
+ function posicionar(nomes, { semente = 1, agua = false, dentro = false, largura = 1 } = {}) {
204
+ const r = rngDe(semente);
205
+ const out = [];
206
+ const livre = (x, z, raio) => out.every((p) => Math.hypot(p.x - x, p.z - z) > raio + (p.raio || 0.8));
207
+ nomes.forEach((nome, i) => {
208
+ const grande = /predio|montanha|celeiro|planeta|foguete|palmeira|pinheiro|arvore/.test(typeof nome === 'string' ? nome : '');
209
+ const raio = grande ? 1.3 : 0.75;
210
+ for (let tent = 0; tent < 40; tent++) {
211
+ let x, z;
212
+ if (dentro) {
213
+ // Encostado na parede do fundo ou nos lados do set.
214
+ x = (r() * 2 - 1) * 4.6 * largura; z = -3.1 + r() * 0.9;
215
+ if (Math.abs(x) < 1.7 && tent < 30) continue;
216
+ } else if (agua) {
217
+ const lado = i % 2 ? 1 : -1;
218
+ x = lado * (1.9 + r() * 4.5) * largura; z = -4.2 + r() * 3.6;
219
+ } else {
220
+ const lado = i % 2 ? 1 : -1;
221
+ const longe = grande ? 0.55 + r() * 0.45 : r();
222
+ x = lado * (1.8 + r() * 5.5) * largura; z = -1.6 - longe * 9;
223
+ if (r() < 0.25 && !grande) { x = lado * (2.2 + r() * 1.5); z = -0.4 + r() * 1.4; }
224
+ }
225
+ if (!livre(x, z, raio) && tent < 39) continue;
226
+ out.push({ x: Math.round(x * 100) / 100, z: Math.round(z * 100) / 100, raio, giro: Math.round((r() * 2 - 1) * 60) / 100, escala: Math.round((0.85 + r() * 0.35) * 100) / 100 });
227
+ break;
228
+ }
229
+ });
230
+ return out;
231
+ }
232
+
233
+ /**
234
+ * O mundo de um trecho do vídeo.
235
+ * @param {string|object} pedido "praia ao pôr do sol" ou {lugar, hora, elementos, ceu, chao, ...}
236
+ * @param {object} op {texto (o pedido e as falas: completa o lugar), pedidoTexto (só o
237
+ * pedido da pessoa: as coisas que ele cita entram na cena), semente, nElenco}
238
+ */
239
+ function mundoDe(pedido, { texto = '', pedidoTexto = '', semente = 1, nElenco = 1 } = {}) {
240
+ const obj = pedido && typeof pedido === 'object' ? pedido : { lugar: String(pedido || '') };
241
+ const descr = [obj.lugar, obj.descricao, obj.hora].filter(Boolean).join(' ');
242
+ let { lugar, hora } = lerLugar(descr);
243
+ const nomeDireto = sem(obj.lugar).replace(/\s+/g, '_');
244
+ if (!lugar && LUGARES[nomeDireto]) lugar = nomeDireto;
245
+ if (!lugar && APELIDOS_LUGAR[nomeDireto]) lugar = APELIDOS_LUGAR[nomeDireto];
246
+ // Sem lugar no pedido da cena, o texto do vídeo (o pedido da pessoa e as falas) completa.
247
+ const doTexto = lerLugar(texto);
248
+ if (!lugar) lugar = doTexto.lugar || 'cobertura';
249
+ if (!hora) hora = doTexto.hora;
250
+ if (APELIDOS_LUGAR[lugar]) lugar = APELIDOS_LUGAR[lugar];
251
+ const L = LUGARES[lugar];
252
+ const H = hora && !L.dentro ? HORAS[hora] : null;
253
+
254
+ // Os elementos: os que a cena listou (qualquer palavra) + os que o texto cita + os do lugar.
255
+ const pedidos = Array.isArray(obj.elementos) ? obj.elementos.map((e) => (typeof e === 'string' ? e : (e && (e.nome || e.tipo)) || '')).filter(Boolean) : [];
256
+ // As falas citam coisa de passagem ("comprei um carro"): só o PEDIDO põe coisa na cena.
257
+ const citados = lerCoisas([descr, pedidoTexto, Array.isArray(obj.elementos) ? '' : String(obj.elementos || '')].join(' ')).pecas;
258
+ const nomes = [];
259
+ for (const e of [...pedidos, ...citados]) {
260
+ const s = sem(e).trim();
261
+ const peca = Object.keys(PECAS).find((k) => PECAS[k].test(s) || sem(k) === s);
262
+ nomes.push(peca ? { peca } : { icone: s.slice(0, 40) });
263
+ }
264
+ // O lugar completa até 7 peças: a cena nunca fica vazia, e o que a IA pediu vem primeiro.
265
+ for (const p of L.props) if (nomes.length < 7 || nomes.length < L.props.length) nomes.push({ peca: p });
266
+ const lugares = posicionar(nomes.map((n) => n.peca || n.icone), { semente, agua: !!L.agua, dentro: !!L.dentro, largura: nElenco > 2 ? 1.25 : 1 });
267
+ const props = nomes.slice(0, lugares.length).map((n, i) => ({ ...n, ...lugares[i] }));
268
+
269
+ const ceu = Array.isArray(obj.ceu) && obj.ceu.every(ehCor) ? obj.ceu : (H ? H.ceu : L.ceu) || null;
270
+ const chao = { ...L.chao, ...(obj.chao && typeof obj.chao === 'object' ? obj.chao : {}), ...(ehCor(obj.chao) ? { cor: obj.chao, cor2: obj.chao } : {}) };
271
+ return {
272
+ nome: lugar + (H ? ':' + hora : ''), lugar, hora: H ? hora : null,
273
+ dentro: !!L.dentro, telas: !!L.telas, cobertura: !!L.cobertura, parede: ehCor(obj.parede) ? obj.parede : L.parede || null,
274
+ ceu, chao, agua: L.agua || null, subaquatico: !!L.subaquatico, holofotes: !!L.holofotes,
275
+ sol: !!(L.sol && !(H && H.lua)), solBaixo: !!(H && H.solBaixo), lua: !!(H && H.lua), estrelas: !!(L.estrelas || (H && H.estrelas)),
276
+ neblina: L.neblina || 0,
277
+ luz: ehCor(obj.luz) ? obj.luz : (H ? H.luz : L.luz), forca: H ? H.forca : 1,
278
+ recorte: L.recorte, acento: ehCor(obj.acento) ? obj.acento : L.acento,
279
+ props,
280
+ };
281
+ }
282
+
283
+ /** Os ícones que o mundo e o elenco precisam baixar (colecao.js) antes do render. */
284
+ function iconesPendentes(partitura) {
285
+ const out = new Set();
286
+ for (const m of partitura.mundos || []) for (const p of m.props) if (p.icone && !p.src) out.add(p.icone);
287
+ for (const e of [...(partitura.elenco || []), ...(partitura.figurantes || [])]) if (e.tipo === 'coisa' && !e.src) out.add(e.icone);
288
+ return [...out];
289
+ }
290
+
291
+ /** Põe o PNG baixado em cada peça e personagem que pediu aquele ícone; o que não achou ícone sai da cena. */
292
+ function aplicarIcones(partitura, achados) {
293
+ for (const m of partitura.mundos || []) {
294
+ m.props = m.props.filter((p) => !p.icone || achados[p.icone]).map((p) => (p.icone ? { ...p, src: achados[p.icone] } : p));
295
+ }
296
+ for (const e of [...(partitura.elenco || []), ...(partitura.figurantes || [])]) {
297
+ if (e.tipo === 'coisa') {
298
+ if (achados[e.icone]) e.src = achados[e.icone];
299
+ else { e.tipo = 'robo'; e.cor = e.cor || '#d97757'; } // sem ícone, a coisa que fala vira o mascote
300
+ }
301
+ }
302
+ return partitura;
303
+ }
304
+
305
+ module.exports = { mundoDe, lerLugar, lerCoisas, lerElenco, iconesPendentes, aplicarIcones, LUGARES, HORAS, PECAS, ESPECIES, sem };
@@ -28,6 +28,8 @@
28
28
  'use strict';
29
29
 
30
30
  const { volumesDaTrilha } = require('./ritmo.js');
31
+ const mundo = require('./mundo.js');
32
+ const colecao = require('./colecao.js');
31
33
 
32
34
  const FPS = 30;
33
35
  const r2 = (x) => Math.round(x * 100) / 100;
@@ -63,7 +65,7 @@ const CENARIOS_VOXEL = {
63
65
  // O nome antigo do set (podcast, estúdio…) tem um par no voxel.
64
66
  const VOXEL_DO_LISO = { podcast: 'telas', estudio: 'oceano', escritorio: 'terra', noite: 'neon' };
65
67
  // O mascote é laranja (o do Claude) a menos que o roteiro diga a cor.
66
- const CORES_MASCOTE = { claude: '#d97757', gpt: '#10a37f', chatgpt: '#10a37f', gemini: '#4f7cff', primo: '#22b8d8', primocode: '#22b8d8' };
68
+ const CORES_MASCOTE = { claude: '#dd5a2b', gpt: '#10a37f', chatgpt: '#10a37f', gemini: '#4f7cff', primo: '#22b8d8', primocode: '#22b8d8' };
67
69
  const ROUPAS_VOXEL = ['#c2452f', '#2f4cdd', '#1f8a5b', '#d08a1e', '#7b5cff', '#e8e2d6'];
68
70
  // A pele de bloco é um tom mais fundo: sob a luz quente, o tom claro estourava.
69
71
  const PELES_VOXEL = ['#d9a07a', '#c08660', '#a8704c', '#8a5a3c', '#6b4229'];
@@ -84,12 +86,44 @@ function rng(semente) {
84
86
  }
85
87
 
86
88
  /** A aparência de quem fala: o que o roteiro disse vale; o resto sai do nome. */
89
+ /* O TIPO de quem fala (11/10, "se eu pedir um cachorro ele vai fazer"):
90
+ pessoa · robo (o mascote) · animal (uma espécie que o kit/mundo.jsx monta) ·
91
+ coisa (qualquer objeto com ícone 3D, em blocos: "a pizza conversando com o
92
+ café"). O que o roteiro disse vale; senão, o NOME decide. */
93
+ const TIPOS = ['pessoa', 'robo', 'animal', 'coisa'];
94
+ function especieDe(txt) {
95
+ const s = mundo.sem(txt);
96
+ return Object.keys(mundo.ESPECIES).find((k) => mundo.ESPECIES[k].test(s) || k === s) || null;
97
+ }
98
+ function iconeDe(txt) {
99
+ const t = String(txt || '').trim().toLowerCase();
100
+ if (!t) return null;
101
+ return colecao.melhorIcone3d(t, 80) || (colecao.PT_EN[t] || colecao.PT_EN[mundo.sem(t)] ? colecao.melhorIcone3d(t, 40) : null);
102
+ }
103
+ function tipoDe(nome, pedido = {}) {
104
+ if (TIPOS.includes(pedido.tipo)) return pedido.tipo;
105
+ if (pedido.especie) return 'animal';
106
+ if (pedido.icone || pedido.coisa) return 'coisa';
107
+ if (especieDe(nome)) return 'animal';
108
+ if (EH_ROBO.test(nome)) return 'robo';
109
+ if (iconeDe(nome)) return 'coisa';
110
+ return 'pessoa';
111
+ }
112
+
87
113
  function aparencia(nome, pedido = {}) {
88
114
  const k = (s) => hashDe(nome + ':' + s);
89
- const tipo = pedido.tipo === 'robo' || pedido.tipo === 'pessoa' ? pedido.tipo : (EH_ROBO.test(nome) ? 'robo' : 'pessoa');
115
+ const tipo = tipoDe(nome, pedido);
90
116
  const rotulo = String(pedido.rotulo || pedido.nome || nome || '').trim().slice(0, 24);
117
+ if (tipo === 'animal') {
118
+ const especie = especieDe(pedido.especie || '') || especieDe(nome) || 'cachorro';
119
+ return { nome, rotulo, tipo, especie, ...(pedido.cor ? { cor: pedido.cor } : {}), ...(pedido.cor2 ? { cor2: pedido.cor2 } : {}) };
120
+ }
121
+ if (tipo === 'coisa') {
122
+ const termo = String(pedido.icone || pedido.coisa || nome);
123
+ return { nome, rotulo, tipo, icone: termo, ...(pedido.src ? { src: pedido.src } : {}) };
124
+ }
91
125
  if (tipo === 'robo') {
92
- return { nome, rotulo, tipo, cor: pedido.cor || pegar(ROBOS, k('cor')), olhos: pedido.olhos || null };
126
+ return { nome, rotulo, tipo, cor: pedido.cor || pegar(ROBOS, k('cor')), olhos: pedido.olhos || null, ...(pedido.bigode ? { bigode: true } : {}) };
93
127
  }
94
128
  return {
95
129
  nome, rotulo, tipo,
@@ -99,6 +133,9 @@ function aparencia(nome, pedido = {}) {
99
133
  roupa: pedido.roupa || pegar(ROUPAS, k('roupa')),
100
134
  oculos: pedido.oculos != null ? !!pedido.oculos : k('oculos') < 0.3,
101
135
  barba: !!pedido.barba, // só quando pedida: sorteada, ficava estranha em quem tem cabelo longo
136
+ bigode: !!pedido.bigode,
137
+ // A cabeça facetada (low-poly) é o padrão; 'cubo' é o rosto de pixel da 10.5.
138
+ ...(pedido.cabeca === 'cubo' ? { cabeca: 'cubo' } : {}),
102
139
  };
103
140
  }
104
141
 
@@ -119,6 +156,22 @@ function destaqueDe(pt, texto) {
119
156
  return m ? m[1].trim() : null;
120
157
  }
121
158
 
159
+ /* O que o corpo faz na fala, além de falar. O roteiro diz (parte.acao); sem
160
+ ele, o texto sugere: "oi"/"tchau" acena, "olha"/"aqui" aponta, "!" de
161
+ vitória comemora. kit/voxel.jsx e kit/mundo.jsx desenham cada uma. */
162
+ const ACOES = ['falar', 'acenar', 'apontar', 'pular', 'dancar', 'comemorar', 'pensar', 'andar', 'correr', 'sentar'];
163
+ function acaoDe(pt, texto) {
164
+ const a = mundo.sem(pt.acao || '').replace(/[^a-z]/g, '');
165
+ const achada = ACOES.find((x) => a.startsWith(x.slice(0, 5)));
166
+ if (achada) return achada;
167
+ const s = mundo.sem(texto);
168
+ if (/^(oi|ola|e ai|fala,|bom dia|boa tarde|boa noite)\b|\b(tchau|ate mais|ate logo|valeu, ate)\b/.test(s)) return 'acenar';
169
+ if (/\b(consegui|conseguimos|ganhamos|vencemos|incrivel|isso ai|uhu|eba|viva)\b.*!/.test(s)) return 'comemorar';
170
+ if (/\b(olha|olhe|veja|repara|ali|aqui o|esse aqui)\b/.test(s)) return 'apontar';
171
+ if (/\b(sera que|hmm+|deixa eu pensar|pensando)\b/.test(s)) return 'pensar';
172
+ return 'falar';
173
+ }
174
+
122
175
  const falaDe = (pt) => String(pt.texto || pt.fala || pt.narracao || pt.titulo || '').trim();
123
176
 
124
177
  /* Sem os tempos da voz, cada fala ganha o tempo de leitura (2,6 palavras/s)
@@ -206,6 +259,36 @@ function paineisDe(falas, roteiro = {}) {
206
259
  return out.length ? out : [{ titulo: String(roteiro.titulo || 'Agora').slice(0, 18), linha: '' }];
207
260
  }
208
261
 
262
+
263
+ /* BICHO NÃO FALA (11/10, "e o cachorro fala? mds kkk"): fala de bicho com voz
264
+ de gente fica ridícula. A parte do bicho SAI da narração e ele fica na cena
265
+ como figurante (abanando o rabo). Ele só fala se o pedido disser que fala:
266
+ "cachorro falante", "um gato que fala", "o cachorro falando". Muda o
267
+ roteiro no lugar (o array de partes é o mesmo) e devolve quem calou. */
268
+ function calarBichos(roteiro, pedido = '') {
269
+ const partes = Array.isArray(roteiro && roteiro.partes) ? roteiro.partes : [];
270
+ const pedidos = (roteiro && roteiro.personagens) || {};
271
+ const ped = mundo.sem(pedido);
272
+ const calados = [];
273
+ for (let i = partes.length - 1; i >= 0; i--) {
274
+ const pt = partes[i];
275
+ const nome = typeof (pt && pt.falante) === 'string' ? pt.falante.trim().toLowerCase() : '';
276
+ if (!nome) continue;
277
+ const dado = pedidos[nome] || pedidos[Object.keys(pedidos).find((k) => k.toLowerCase() === nome)] || {};
278
+ const fala = mundo.sem(partes.filter((x) => String(x.falante || '').trim().toLowerCase() === nome).map(falaDe).join(' '));
279
+ const latido = /\bau[ -]?au\b|\bau!|\bmiau|\bmu+u\b|\boinc\b|\bquack\b|\bcocoric/.test(fala);
280
+ const tipo = dado.tipo || (dado.especie ? 'animal' : (especieDe(nome) || latido ? 'animal' : null));
281
+ if (tipo !== 'animal' || dado.fala === true) continue;
282
+ const esp = especieDe(dado.especie || '') || especieDe(nome) || (/\bmiau/.test(fala) ? 'gato' : 'cachorro');
283
+ const falante = new RegExp(`\\b(${Object.keys(mundo.ESPECIES).join('|')}|bicho|animal|${nome})\\w*\\s+(falante|que fala|falando|fala)\\b`).test(ped);
284
+ if (falante) continue;
285
+ partes.splice(i, 1);
286
+ if (!calados.some((c) => c.nome === nome)) calados.push({ nome, especie: esp });
287
+ }
288
+ if (calados.length) roteiro.figurantesExtra = [...(roteiro.figurantesExtra || []), ...calados.map((c) => ({ nome: c.nome, tipo: 'animal', especie: c.especie }))];
289
+ return calados;
290
+ }
291
+
209
292
  /** A partitura inteira: o que o kit/personagem.jsx desenha, quadro a quadro. */
210
293
  function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = [] }) {
211
294
  const partes = (roteiro.partes || []).filter((p) => p && typeof p === 'object' && falaDe(p));
@@ -220,9 +303,20 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
220
303
  if (!nomes.includes(f)) nomes.push(f);
221
304
  }
222
305
  const pedidos = roteiro.personagens && typeof roteiro.personagens === 'object' ? roteiro.personagens : {};
223
- const pedidoDe = (n) => pedidos[n] || pedidos[Object.keys(pedidos).find((k) => k.toLowerCase() === n)] || {};
224
- // No set cabem dois; o terceiro em diante divide o lugar (raro num diálogo).
225
- const elenco = nomes.slice(0, 2).map((n, i) => ({ ...aparencia(n, pedidoDe(n)), lado: nomes.length > 1 ? (i === 0 ? -1 : 1) : 0 }));
306
+ /* Quem LATE é cachorro: um falante sem tipo cuja fala tem a voz de um
307
+ bicho ("Au au!", "Miau") vira aquele bicho — o modelo pequeno escreve o
308
+ diálogo e esquece de dizer que o Rex é um cachorro. */
309
+ const VOZ_DE_BICHO = [[/\bau[ -]?au\b|\bau!|\bauau\b|\bwoof\b/, 'cachorro'], [/\bmiau+\b/, 'gato'], [/\bmu+u\b/, 'vaca'], [/\boinc\b/, 'porco'],
310
+ [/\bquack\b|\bqua[ -]?qua\b/, 'pato'], [/\bcocoric[oó]\b/, 'galinha'], [/\bpiu[ -]?piu\b/, 'passaro'], [/\bme+h+\b|\bbe+h+\b/, 'ovelha'], [/\brua+r+\b|\broar\b/, 'leao']];
311
+ const pedidoDe = (n) => {
312
+ const dado = pedidos[n] || pedidos[Object.keys(pedidos).find((k) => k.toLowerCase() === n)] || {};
313
+ if (dado.tipo || dado.especie) return dado;
314
+ const fala = mundo.sem(partes.filter((pt) => String(pt.falante || '').trim().toLowerCase() === n).map(falaDe).join(' '));
315
+ const bicho = VOZ_DE_BICHO.find(([re]) => re.test(fala));
316
+ return bicho ? { ...dado, tipo: 'animal', especie: bicho[1] } : dado;
317
+ };
318
+ // Até QUATRO falam no set ("três amigos no parque"); do quinto em diante dividem o lugar.
319
+ const elenco = nomes.slice(0, 4).map((n, i) => ({ ...aparencia(n, pedidoDe(n)), lado: nomes.length > 1 ? (i < nomes.length / 2 ? -1 : 1) : 0, ordem: i }));
226
320
  const quemDe = (pt) => {
227
321
  const f = typeof pt.falante === 'string' && pt.falante.trim() ? pt.falante.trim().toLowerCase() : 'apresentador';
228
322
  return nomes.indexOf(f) % Math.max(1, elenco.length);
@@ -235,7 +329,7 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
235
329
  .map((w) => ({ t: String(w.texto || w.word || ''), de: r2(w.de), ate: r2(w.ate) }));
236
330
  return {
237
331
  quem: quemDe(pt), de: r2(t.de != null ? t.de : (palavras[0] || {}).de || 0), ate: r2(t.ate != null ? t.ate : (palavras[palavras.length - 1] || {}).ate || 0),
238
- texto, palavras, emocao: emocaoDe(pt, texto), destaque: destaqueDe(pt, texto),
332
+ texto, palavras, emocao: emocaoDe(pt, texto), destaque: destaqueDe(pt, texto), acao: acaoDe(pt, texto),
239
333
  };
240
334
  });
241
335
  const duracao = r2((falas.length ? falas[falas.length - 1].ate : 3) + 1.2);
@@ -258,14 +352,72 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
258
352
  cenarioFinal = { nome: nomeV, ...CENARIOS_VOXEL[nomeV] };
259
353
  if (roteiro.acento && /^#[0-9a-f]{6}$/i.test(roteiro.acento)) cenarioFinal.acento = roteiro.acento;
260
354
  for (const e of elenco) {
261
- if (e.tipo === 'robo' && !(pedidoDe(e.nome) || {}).cor) e.cor = CORES_MASCOTE[e.nome] || '#d97757';
355
+ if (e.tipo === 'robo' && !(pedidoDe(e.nome) || {}).cor) e.cor = CORES_MASCOTE[e.nome] || '#dd5a2b';
262
356
  if (e.tipo === 'pessoa' && !(pedidoDe(e.nome) || {}).roupa) e.roupa = pegar(ROUPAS_VOXEL, hashDe(e.nome + ':roupa'));
263
357
  if (e.tipo === 'pessoa' && !(pedidoDe(e.nome) || {}).pele) e.pele = pegar(PELES_VOXEL, hashDe(e.nome + ':pele'));
264
358
  if (e.tipo === 'pessoa' && !(pedidoDe(e.nome) || {}).corCabelo) e.corCabelo = pegar(CABELOS_VOXEL, hashDe(e.nome + ':corCabelo'));
265
359
  }
266
360
  }
361
+ /* O MUNDO (mundo.js): o lugar e as coisas. roteiro.cena (texto livre ou
362
+ objeto) é o do vídeo; parte.cenario muda o lugar a partir daquela fala.
363
+ O pedido da pessoa e as falas completam o que a cena não disse. */
364
+ const pedidoTexto = String(roteiro.pedido || roteiro.descricao || '');
365
+ const textoTodo = [pedidoTexto, roteiro.titulo, ...falas.map((f) => f.texto)].filter(Boolean).join(' ');
366
+ const cenaBase = roteiro.cena || roteiro.cenario || '';
367
+ const mundos = [];
368
+ const indiceDe = (pedido) => {
369
+ const chave = JSON.stringify(pedido || '');
370
+ const ja = mundos.findIndex((m) => m.chave === chave);
371
+ if (ja >= 0) return ja;
372
+ mundos.push({ chave, ...mundo.mundoDe(pedido, { texto: textoTodo, pedidoTexto, semente: semente + mundos.length * 7, nElenco: elenco.length }) });
373
+ return mundos.length - 1;
374
+ };
375
+ indiceDe(cenaBase);
376
+ let atual = 0;
377
+ partes.forEach((pt, i) => {
378
+ const pedidoParte = pt.cena || pt.cenario;
379
+ if (pedidoParte) atual = indiceDe(pedidoParte);
380
+ if (falas[i]) falas[i].mundo = atual;
381
+ });
382
+ for (const m of mundos) delete m.chave;
383
+ // O set da referência (telas) segue com as cores dele; os outros lugares trazem as suas.
384
+ if (mundos[0].lugar !== 'telas') cenarioFinal = { ...cenarioFinal, nome: mundos[0].nome, parede: mundos[0].parede || mundos[0].ceu?.[1] || cenarioFinal.parede, acento: mundos[0].acento };
385
+
386
+ /* OS FIGURANTES: quem o pedido põe na cena e não fala ("um humano com o
387
+ cachorro", "dois humanos" com um narrador só). roteiro.figurantes manda;
388
+ senão, os bichos e as pessoas do pedido que sobraram do elenco. */
389
+ const figurantes = [];
390
+ if (Array.isArray(roteiro.figurantes)) {
391
+ for (const f of roteiro.figurantes.slice(0, 4)) {
392
+ const pedidoF = typeof f === 'string' ? { tipo: tipoDe(f, {}), especie: especieDe(f) || undefined, icone: f } : f || {};
393
+ const nomeF = typeof f === 'string' ? f : String(f.nome || f.especie || f.icone || 'figurante');
394
+ figurantes.push({ ...aparencia(nomeF, pedidoF) });
395
+ }
396
+ } else if (pedidoTexto) {
397
+ const lido = mundo.lerElenco(pedidoTexto);
398
+ const temEspecie = (esp) => elenco.some((e) => e.tipo === 'animal' && e.especie === esp);
399
+ for (const b of lido.bichos) {
400
+ for (let k = 0; k < b.quantos - (temEspecie(b.especie) ? 1 : 0) && figurantes.length < 3; k++) {
401
+ figurantes.push(aparencia(`${b.especie} ${k + 1}`, { tipo: 'animal', especie: b.especie }));
402
+ }
403
+ }
404
+ const pessoas = elenco.filter((e) => e.tipo === 'pessoa').length;
405
+ for (let k = pessoas; k < lido.pessoas && figurantes.length < 4; k++) figurantes.push(aparencia(`pessoa ${k + 1}`, { tipo: 'pessoa' }));
406
+ }
407
+ // Os bichos que calaram (calarBichos) entram como figurantes, antes de tudo.
408
+ for (const f of (Array.isArray(roteiro.figurantesExtra) ? roteiro.figurantesExtra : []).slice(0, 3)) {
409
+ if (!figurantes.some((x) => x.nome === f.nome)) figurantes.unshift(aparencia(f.nome, { tipo: 'animal', especie: f.especie }));
410
+ }
411
+ // O mesmo bicho que o pedido já pôs como figurante não aparece duas vezes.
412
+ for (let k = figurantes.length - 1; k > 0; k--) if (figurantes.slice(0, k).some((x) => x.tipo === 'animal' && x.especie === figurantes[k].especie && x !== figurantes[k] && /\d$/.test(figurantes[k].nome))) figurantes.splice(k, 1);
413
+ if (visual === 'voxel') {
414
+ for (const e of figurantes) {
415
+ if (e.tipo === 'pessoa') { e.roupa = pegar(ROUPAS_VOXEL, hashDe(e.nome + ':roupa')); e.pele = pegar(PELES_VOXEL, hashDe(e.nome + ':pele')); e.corCabelo = pegar(CABELOS_VOXEL, hashDe(e.nome + ':corCabelo')); }
416
+ }
417
+ }
418
+
267
419
  return {
268
- versao: 1, fps: FPS, largura, altura, duracao, semente, cenario: cenarioFinal, elenco, falas, planos, visual,
420
+ versao: 2, fps: FPS, largura, altura, duracao, semente, cenario: cenarioFinal, elenco, falas, planos, visual, mundos, figurantes,
269
421
  paineis: paineisDe(falas, roteiro),
270
422
  legenda: roteiro.legenda !== false,
271
423
  titulo: typeof roteiro.titulo === 'string' ? roteiro.titulo.slice(0, 60) : '',
@@ -296,8 +448,9 @@ function montarPersonagem({ roteiro, largura = 1920, altura = 1080, tempos = [],
296
448
  };
297
449
  return {
298
450
  ok: true, plano, partitura,
299
- resumo: { visual: partitura.visual, cenas: 1, segundos: DUR, fps: FPS, cenario: partitura.cenario.nome, elenco: partitura.elenco.map((e) => `${e.rotulo} (${e.tipo})`), planos: partitura.planos.length, falas: partitura.falas.length },
451
+ resumo: { visual: partitura.visual, cenas: 1, segundos: DUR, fps: FPS, cenario: partitura.cenario.nome, elenco: partitura.elenco.map((e) => `${e.rotulo} (${e.tipo === 'animal' ? e.especie : e.tipo})`),
452
+ figurantes: partitura.figurantes.map((e) => (e.tipo === 'animal' ? e.especie : e.tipo)), lugares: partitura.mundos.map((m) => m.nome), planos: partitura.planos.length, falas: partitura.falas.length },
300
453
  };
301
454
  }
302
455
 
303
- module.exports = { montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };
456
+ module.exports = { calarBichos, montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, tipoDe, acaoDe, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };