Etapa 1 inteira (logica com 100% de cobertura, cena 3D, toque, sons, bancada de desempenho, e2e) e a base da etapa 2 (mapa novo no Tiled, texturas, imagens dos itens, sons conferidos, golpe recortado e medido). Pagina /jogo fora do menu e do sitemap ate a medicao no celular.
578 lines
19 KiB
TypeScript
578 lines
19 KiB
TypeScript
/**
|
|
* Zumbis, drones e caixas de munição desenhados a partir da `Partida`. Os zumbis vêm de uma reserva de clones que só
|
|
* cresce: quando um sai da partida, o clone se esconde e volta para outro. Cada zumbi leva o nome do problema que é,
|
|
* e cada drone o nome da solução.
|
|
*/
|
|
import {
|
|
BoxGeometry,
|
|
type BufferGeometry,
|
|
type CanvasTexture,
|
|
Color,
|
|
InstancedMesh,
|
|
type Material,
|
|
Matrix4,
|
|
type Mesh,
|
|
MeshStandardMaterial,
|
|
Object3D,
|
|
type Scene,
|
|
Sprite,
|
|
SpriteMaterial,
|
|
type Texture,
|
|
} from "three";
|
|
import { problemaPorId } from "@/lib/jogo/problemas";
|
|
import type { Drone, Partida, TipoZumbi, Zumbi } from "@/lib/jogo/tipos";
|
|
import { DIFICULDADES, HORDAS } from "@/lib/jogo/valores";
|
|
import { texturaDeCanvas } from "./ambiente";
|
|
import {
|
|
criarZumbi,
|
|
type ModeloDoZumbi,
|
|
type Zumbi as ZumbiNaTela,
|
|
} from "./zumbis3d";
|
|
|
|
/** O que os atores precisam já baixado. `roupas` são as variações B e C da roupa; a que faltar fica de fora. */
|
|
export type ModelosDosAtores = {
|
|
zumbi: ModeloDoZumbi;
|
|
bloco: BufferGeometry;
|
|
roupas: readonly Texture[];
|
|
};
|
|
|
|
export type Atores = {
|
|
/** Depois de cada passo da lógica: guarda posição e rumo de antes e de agora, para interpolar. */
|
|
guardarPasso(p: Partida): void;
|
|
/** `alfa` de 0 a 1 entre o passo anterior e o atual; `dt` em segundos do jogo, para as animações. */
|
|
sincronizar(p: Partida, alfa: number, dt: number): void;
|
|
descartar(): void;
|
|
};
|
|
|
|
const ESCALA: Record<TipoZumbi, number> = {
|
|
comum: 1,
|
|
rapido: 0.95,
|
|
blindado: 1.08,
|
|
chefao: 1.5,
|
|
};
|
|
/** Altura do modelo em escala 1, em metros. */
|
|
const ALTURA_DO_MODELO = 1.8;
|
|
const ALCANCE_DO_ROTULO = 10;
|
|
/** Só os zumbis mais perto (e o chefão) levam rótulo, para não empilhar nomes na tela. */
|
|
const ROTULOS_DE_ZUMBI = 3;
|
|
/** O rótulo do chefão não passa disto, para não entrar no forro (3 m). */
|
|
const TETO_DO_ROTULO = 2.8;
|
|
/** Metade do campo de visão vertical da câmera (75° em `cena.ts`), para medir o rótulo em pixels. */
|
|
const TANGENTE_DO_CAMPO = Math.tan((37.5 * Math.PI) / 180);
|
|
/** Rótulo do drone em relação ao do zumbi. */
|
|
const ESCALA_DO_ROTULO_DO_DRONE = 0.8;
|
|
/** Na tela, o drone fica acima da cabeça do jogador (olhos a 1,6 m); a lógica continua no chão. */
|
|
const ALTURA_DO_DRONE = 2.3;
|
|
const BLOCOS_POR_DRONE = 8;
|
|
const VERDE = "#c5f04a";
|
|
/** shortcut: no máximo 32 caixas desenhadas ao mesmo tempo; se o chão juntar mais, as velhas somem até alguém pegar. */
|
|
const MAX_CAIXAS = 32;
|
|
|
|
type Rotulo = {
|
|
material: SpriteMaterial;
|
|
textura: CanvasTexture;
|
|
aspecto: number;
|
|
texto: string;
|
|
};
|
|
|
|
type Trio = [number, number, number];
|
|
/** Posição e rumo no passo anterior e no atual. */
|
|
type Rastro = { antes: Trio; agora: Trio };
|
|
|
|
type Ator = Rastro & {
|
|
zumbi: ZumbiNaTela;
|
|
roupa: Mesh | null;
|
|
olhos: Mesh | null;
|
|
rotulo: Sprite;
|
|
aspecto: number;
|
|
id: number;
|
|
/** Último quadro em que o zumbi estava na partida. */
|
|
visto: number;
|
|
clipe: string;
|
|
};
|
|
|
|
function misturarAngulo(a: number, b: number, t: number): number {
|
|
const d = Math.atan2(Math.sin(b - a), Math.cos(b - a));
|
|
return a + d * t;
|
|
}
|
|
|
|
/** Põe antes e agora no mesmo lugar, sem alocar. */
|
|
function fixar(r: Rastro, x: number, z: number, rumo: number) {
|
|
r.antes[0] = r.agora[0] = x;
|
|
r.antes[1] = r.agora[1] = z;
|
|
r.antes[2] = r.agora[2] = rumo;
|
|
}
|
|
|
|
/** Um passo adiante sem alocar: o vetor de antes vira o de agora. Partida parada (passo igual): antes = agora. */
|
|
function seguir(
|
|
r: Rastro,
|
|
x: number,
|
|
z: number,
|
|
rumo: number,
|
|
parado: boolean,
|
|
) {
|
|
if (parado) {
|
|
fixar(r, r.agora[0], r.agora[1], r.agora[2]);
|
|
return;
|
|
}
|
|
const velho = r.antes;
|
|
r.antes = r.agora;
|
|
velho[0] = x;
|
|
velho[1] = z;
|
|
velho[2] = rumo;
|
|
r.agora = velho;
|
|
}
|
|
|
|
function malhaDoMaterial(raiz: Object3D, nome: string): Mesh | null {
|
|
let achada: Mesh | null = null;
|
|
raiz.traverse((objeto) => {
|
|
const malha = objeto as Mesh;
|
|
if (!achada && malha.isMesh && (malha.material as Material).name === nome)
|
|
achada = malha;
|
|
});
|
|
return achada;
|
|
}
|
|
|
|
function fonteMono(): string {
|
|
const raiz = getComputedStyle(document.documentElement);
|
|
return raiz.getPropertyValue("--font-mono").trim() || "monospace";
|
|
}
|
|
|
|
/** Etiqueta reta (cantos retos, como o site), texto em mono. */
|
|
function texturaDoRotulo(texto: string, fundo: string, tinta: string) {
|
|
const fonte = `500 46px ${fonteMono()}`;
|
|
const medida = document.createElement("canvas").getContext("2d");
|
|
if (medida) medida.font = fonte;
|
|
const largura = Math.ceil(
|
|
(medida?.measureText(texto).width ?? texto.length * 28) + 32,
|
|
);
|
|
const textura = texturaDeCanvas(largura, 64, (ctx) => {
|
|
ctx.fillStyle = fundo;
|
|
ctx.fillRect(0, 0, largura, 64);
|
|
ctx.font = fonte;
|
|
ctx.fillStyle = tinta;
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(texto, 16, 34);
|
|
});
|
|
return { textura, aspecto: largura / 64 };
|
|
}
|
|
|
|
/** Altura do rótulo em unidades de `Sprite` sem atenuação: ~18 px no computador, ~14 px no celular. */
|
|
function alturaDoRotulo(): number {
|
|
const altura = window.innerHeight || 900;
|
|
const pixels = Math.min(18, Math.max(14, altura * 0.02));
|
|
return (pixels * 2 * TANGENTE_DO_CAMPO) / altura;
|
|
}
|
|
|
|
function clipeDo(z: Zumbi): string {
|
|
if (z.estado === "atacando") return "atacar";
|
|
if (z.estado === "morrendo") return "morrer";
|
|
return z.tipo === "rapido" ? "correr" : "andar";
|
|
}
|
|
|
|
export function criarAtores(cena: Scene, modelo: ModelosDosAtores): Atores {
|
|
const reserva: Ator[] = [];
|
|
const porId = new Map<number, Ator>();
|
|
const rotulos = new Map<string, Rotulo>();
|
|
const materiais = new Map<string, Material>();
|
|
let passoVisto = -1;
|
|
let relogio = 0;
|
|
let quadro = 0;
|
|
// os zumbis mais perto deste quadro, em reserva fixa
|
|
const maisPerto: (Ator | null)[] = Array(ROTULOS_DE_ZUMBI).fill(null);
|
|
const distanciasMaisPerto = new Float64Array(ROTULOS_DE_ZUMBI);
|
|
|
|
const roupaOriginal = malhaDoMaterial(modelo.zumbi.cena, "roupa")?.material as
|
|
| MeshStandardMaterial
|
|
| undefined;
|
|
const olhosOriginais = malhaDoMaterial(modelo.zumbi.cena, "olhos")
|
|
?.material as MeshStandardMaterial | undefined;
|
|
|
|
/** Material derivado, criado uma vez e guardado: variação de roupa, blindado escurecido, olhos do chefão. */
|
|
function derivado(
|
|
chave: string,
|
|
base: MeshStandardMaterial,
|
|
ajustar: (m: MeshStandardMaterial) => void,
|
|
): Material {
|
|
let material = materiais.get(chave);
|
|
if (!material) {
|
|
const novo = base.clone();
|
|
ajustar(novo);
|
|
materiais.set(chave, novo);
|
|
material = novo;
|
|
}
|
|
return material;
|
|
}
|
|
|
|
function roupaPara(z: Zumbi): Material | null {
|
|
if (!roupaOriginal) return null;
|
|
const variacao = z.id % 3;
|
|
const mapa = variacao > 0 ? modelo.roupas[variacao - 1] : undefined;
|
|
const base = mapa
|
|
? (derivado(`roupa-${variacao}`, roupaOriginal, (m) => {
|
|
m.map = mapa;
|
|
}) as MeshStandardMaterial)
|
|
: roupaOriginal;
|
|
if (z.tipo !== "blindado") return base;
|
|
return derivado(`blindado-${variacao}`, base, (m) => {
|
|
m.color.multiplyScalar(0.35);
|
|
m.metalness = 0.75;
|
|
m.roughness = 0.35;
|
|
});
|
|
}
|
|
|
|
function olhosPara(z: Zumbi): Material | null {
|
|
if (!olhosOriginais) return null;
|
|
if (z.tipo !== "chefao") return olhosOriginais;
|
|
return derivado("olhos-chefao", olhosOriginais, (m) => {
|
|
m.color.set("#ff2a1a");
|
|
m.emissive = new Color("#ff1a0a");
|
|
m.emissiveIntensity = 4;
|
|
});
|
|
}
|
|
|
|
function rotuloDo(
|
|
chave: string,
|
|
texto: string,
|
|
fundo: string,
|
|
tinta: string,
|
|
discreto = false,
|
|
): Rotulo {
|
|
let rotulo = rotulos.get(chave);
|
|
if (!rotulo) {
|
|
const { textura, aspecto } = texturaDoRotulo(texto, fundo, tinta);
|
|
// sem atenuação: o rótulo tem o mesmo tamanho na tela a 1 m ou a 10 m. O do zumbi testa profundidade para
|
|
// não atravessar parede; o do drone, discreto, não, senão o próprio drone come o texto.
|
|
const material = new SpriteMaterial({
|
|
map: textura,
|
|
fog: false,
|
|
sizeAttenuation: false,
|
|
opacity: discreto ? 0.85 : 1,
|
|
depthTest: !discreto,
|
|
});
|
|
rotulo = { material, textura, aspecto, texto };
|
|
rotulos.set(chave, rotulo);
|
|
}
|
|
return rotulo;
|
|
}
|
|
|
|
function aplicarRotulo(sprite: Sprite, rotulo: Rotulo) {
|
|
sprite.material = rotulo.material;
|
|
sprite.name = rotulo.texto;
|
|
}
|
|
|
|
function novoAtor(): Ator {
|
|
const zumbi = criarZumbi(modelo.zumbi);
|
|
zumbi.raiz.traverse((objeto) => {
|
|
objeto.castShadow = true;
|
|
});
|
|
// os olhos são pequenos demais para sombra; poupa uma chamada por zumbi
|
|
const olhos = malhaDoMaterial(zumbi.raiz, "olhos");
|
|
if (olhos) olhos.castShadow = false;
|
|
const rotulo = new Sprite();
|
|
rotulo.renderOrder = 10;
|
|
// ancorado embaixo: o rótulo sobe a partir do ponto acima da cabeça
|
|
rotulo.center.set(0.5, 0);
|
|
cena.add(zumbi.raiz, rotulo);
|
|
const ator: Ator = {
|
|
zumbi,
|
|
roupa: malhaDoMaterial(zumbi.raiz, "roupa"),
|
|
olhos,
|
|
rotulo,
|
|
aspecto: 1,
|
|
id: -1,
|
|
visto: 0,
|
|
clipe: "",
|
|
antes: [0, 0, 0],
|
|
agora: [0, 0, 0],
|
|
};
|
|
reserva.push(ator);
|
|
return ator;
|
|
}
|
|
|
|
/** Prepara um clone livre (ou novo) para o zumbi `z`. */
|
|
function vestir(z: Zumbi): Ator {
|
|
const ator = reserva.find((a) => a.id < 0) ?? novoAtor();
|
|
ator.id = z.id;
|
|
fixar(ator, z.x, z.z, z.rumo);
|
|
ator.zumbi.raiz.scale.setScalar(ESCALA[z.tipo]);
|
|
ator.zumbi.raiz.visible = true;
|
|
const roupa = roupaPara(z);
|
|
if (ator.roupa && roupa) ator.roupa.material = roupa;
|
|
const olhos = olhosPara(z);
|
|
if (ator.olhos && olhos) ator.olhos.material = olhos;
|
|
const problema = problemaPorId(z.problemaId);
|
|
const chefao = z.tipo === "chefao";
|
|
const rotulo = rotuloDo(
|
|
`${chefao ? "chefao" : "zumbi"}:${z.problemaId}`,
|
|
problema?.nome ?? z.problemaId,
|
|
"rgba(10,10,10,0.78)",
|
|
chefao ? "#ff5a4a" : "#f4f2ed",
|
|
);
|
|
aplicarRotulo(ator.rotulo, rotulo);
|
|
ator.aspecto = rotulo.aspecto;
|
|
// degraus por id, na tela: rótulos de zumbis juntos não se sobrepõem por inteiro
|
|
ator.rotulo.center.y = -(z.id % 3) * 1.15;
|
|
ator.clipe = clipeDo(z);
|
|
ator.zumbi.tocar(ator.clipe, { umaVez: ator.clipe === "morrer" });
|
|
// fases diferentes, para a horda não andar em marcha
|
|
ator.zumbi.avancar((z.id * 0.37) % 1.2);
|
|
porId.set(z.id, ator);
|
|
return ator;
|
|
}
|
|
|
|
function soltar(ator: Ator) {
|
|
porId.delete(ator.id);
|
|
ator.id = -1;
|
|
ator.zumbi.raiz.visible = false;
|
|
ator.rotulo.visible = false;
|
|
}
|
|
|
|
// Drones: os 8 blocos de cada um numa malha só.
|
|
const maxDrones = HORDAS.tamanhos.length;
|
|
const verde = new MeshStandardMaterial({
|
|
color: VERDE,
|
|
emissive: VERDE,
|
|
emissiveIntensity: 0.45,
|
|
roughness: 0.5,
|
|
});
|
|
const blocos = new InstancedMesh(
|
|
modelo.bloco,
|
|
verde,
|
|
maxDrones * BLOCOS_POR_DRONE,
|
|
);
|
|
blocos.count = 0;
|
|
blocos.frustumCulled = false;
|
|
const rotulosDosDrones = Array.from({ length: maxDrones }, () => {
|
|
const sprite = new Sprite();
|
|
sprite.renderOrder = 10;
|
|
sprite.center.set(0.5, 0);
|
|
sprite.visible = false;
|
|
return sprite;
|
|
});
|
|
const problemasDosRotulos: string[] = Array(maxDrones).fill("");
|
|
const aspectosDosRotulos = new Float64Array(maxDrones);
|
|
const posicoesDosDrones = new Map<number, Rastro>();
|
|
|
|
const materialDaCaixa = new MeshStandardMaterial({
|
|
color: "#d9a400",
|
|
emissive: "#d9a400",
|
|
emissiveIntensity: 0.6,
|
|
roughness: 0.6,
|
|
});
|
|
const caixas = new InstancedMesh(
|
|
new BoxGeometry(0.32, 0.2, 0.24),
|
|
materialDaCaixa,
|
|
MAX_CAIXAS,
|
|
);
|
|
caixas.count = 0;
|
|
caixas.frustumCulled = false;
|
|
cena.add(blocos, caixas, ...rotulosDosDrones);
|
|
|
|
const peca = new Object3D();
|
|
const corpo = new Object3D();
|
|
const m = new Matrix4();
|
|
|
|
function guardarPasso(p: Partida) {
|
|
// terminada, a partida não anda mais: sem isto antes ≠ agora e a interpolação treme
|
|
const parado = p.passo === passoVisto;
|
|
passoVisto = p.passo;
|
|
for (const z of p.zumbis) {
|
|
const ator = porId.get(z.id);
|
|
if (ator) seguir(ator, z.x, z.z, z.rumo, parado);
|
|
}
|
|
for (const d of p.drones) {
|
|
const pos = posicoesDosDrones.get(d.id);
|
|
if (pos) seguir(pos, d.x, d.z, d.rumo, parado);
|
|
}
|
|
}
|
|
|
|
/** O rastro do drone; o que acabou de nascer começa parado onde está. */
|
|
function rastroDo(d: Drone): Rastro {
|
|
let pos = posicoesDosDrones.get(d.id);
|
|
if (!pos) {
|
|
pos = { antes: [d.x, d.z, d.rumo], agora: [d.x, d.z, d.rumo] };
|
|
posicoesDosDrones.set(d.id, pos);
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
/** Guarda `ator` entre os mais perto deste quadro, em ordem de distância. */
|
|
function guardarMaisPerto(ator: Ator, distancia: number) {
|
|
let i = ROTULOS_DE_ZUMBI - 1;
|
|
if (distancia >= distanciasMaisPerto[i]) return;
|
|
while (i > 0 && distanciasMaisPerto[i - 1] > distancia) {
|
|
distanciasMaisPerto[i] = distanciasMaisPerto[i - 1];
|
|
maisPerto[i] = maisPerto[i - 1];
|
|
i--;
|
|
}
|
|
distanciasMaisPerto[i] = distancia;
|
|
maisPerto[i] = ator;
|
|
}
|
|
|
|
function sincronizarZumbis(
|
|
p: Partida,
|
|
alfa: number,
|
|
dt: number,
|
|
alturaDoTexto: number,
|
|
) {
|
|
quadro++;
|
|
const ritmo = DIFICULDADES[p.dificuldade].velocidade;
|
|
const { x: jx, z: jz } = p.jogador;
|
|
distanciasMaisPerto.fill(Number.POSITIVE_INFINITY);
|
|
maisPerto.fill(null);
|
|
for (const z of p.zumbis) {
|
|
const ator = porId.get(z.id) ?? vestir(z);
|
|
ator.visto = quadro;
|
|
const clipe = clipeDo(z);
|
|
if (clipe !== ator.clipe) {
|
|
ator.clipe = clipe;
|
|
ator.zumbi.tocar(clipe, { umaVez: clipe === "morrer" });
|
|
}
|
|
const [x0, z0, r0] = ator.antes;
|
|
const [x1, z1, r1] = ator.agora;
|
|
const x = x0 + (x1 - x0) * alfa;
|
|
const zz = z0 + (z1 - z0) * alfa;
|
|
const raiz = ator.zumbi.raiz;
|
|
raiz.position.set(x, 0, zz);
|
|
raiz.rotation.y = misturarAngulo(r0, r1, alfa);
|
|
// andar e correr no ritmo da dificuldade, para o pé não deslizar no chão
|
|
ator.zumbi.avancar(z.estado === "andando" ? dt * ritmo : dt);
|
|
const altura = ALTURA_DO_MODELO * ESCALA[z.tipo] + 0.25;
|
|
const rotulo = ator.rotulo;
|
|
rotulo.position.set(x, Math.min(altura, TETO_DO_ROTULO), zz);
|
|
rotulo.scale.set(alturaDoTexto * ator.aspecto, alturaDoTexto, 1);
|
|
rotulo.visible = z.tipo === "chefao" && z.estado !== "morrendo";
|
|
if (z.tipo === "chefao" || z.estado === "morrendo") continue;
|
|
const distancia = Math.hypot(x - jx, zz - jz);
|
|
if (distancia <= ALCANCE_DO_ROTULO) guardarMaisPerto(ator, distancia);
|
|
}
|
|
for (let i = 0; i < ROTULOS_DE_ZUMBI; i++) {
|
|
const ator = maisPerto[i];
|
|
if (ator) ator.rotulo.visible = true;
|
|
}
|
|
for (const ator of reserva)
|
|
if (ator.id >= 0 && ator.visto !== quadro) soltar(ator);
|
|
}
|
|
|
|
function sincronizarDrones(p: Partida, alfa: number, alturaDoTexto: number) {
|
|
const n = Math.min(p.drones.length, maxDrones);
|
|
blocos.count = n * BLOCOS_POR_DRONE;
|
|
for (let i = 0; i < n; i++) {
|
|
const d = p.drones[i];
|
|
const pos = rastroDo(d);
|
|
const [x0, z0, r0] = pos.antes;
|
|
const [x1, z1, r1] = pos.agora;
|
|
const x = x0 + (x1 - x0) * alfa;
|
|
const z = z0 + (z1 - z0) * alfa;
|
|
const y = ALTURA_DO_DRONE + Math.sin(relogio * 2 + d.id) * 0.06;
|
|
corpo.position.set(x, y, z);
|
|
corpo.rotation.set(
|
|
Math.sin(relogio * 1.3 + d.id) * 0.08,
|
|
misturarAngulo(r0, r1, alfa),
|
|
0,
|
|
);
|
|
corpo.updateMatrix();
|
|
for (let b = 0; b < BLOCOS_POR_DRONE; b++) {
|
|
peca.position.set(
|
|
(b & 1 ? 1 : -1) * 0.07,
|
|
(b & 2 ? 1 : -1) * 0.07,
|
|
(b & 4 ? 1 : -1) * 0.07,
|
|
);
|
|
peca.scale.setScalar(0.12);
|
|
peca.updateMatrix();
|
|
m.multiplyMatrices(corpo.matrix, peca.matrix);
|
|
blocos.setMatrixAt(i * BLOCOS_POR_DRONE + b, m);
|
|
}
|
|
const sprite = rotulosDosDrones[i];
|
|
// troca a etiqueta só quando muda o drone da vaga: nada de texto novo por quadro
|
|
if (problemasDosRotulos[i] !== d.problemaId) {
|
|
problemasDosRotulos[i] = d.problemaId;
|
|
const problema = problemaPorId(d.problemaId);
|
|
const rotulo = rotuloDo(
|
|
`drone:${d.problemaId}`,
|
|
problema?.solucao ?? d.problemaId,
|
|
"rgba(10,10,10,0.55)",
|
|
"#ccff00",
|
|
true,
|
|
);
|
|
aplicarRotulo(sprite, rotulo);
|
|
aspectosDosRotulos[i] = rotulo.aspecto;
|
|
}
|
|
const alturaDoRotuloDoDrone = alturaDoTexto * ESCALA_DO_ROTULO_DO_DRONE;
|
|
sprite.scale.set(
|
|
alturaDoRotuloDoDrone * aspectosDosRotulos[i],
|
|
alturaDoRotuloDoDrone,
|
|
1,
|
|
);
|
|
// acima do topo do drone visto de baixo, para o texto não cair sobre os blocos
|
|
sprite.position.set(x, y + 0.34, z);
|
|
sprite.visible = true;
|
|
}
|
|
for (let i = n; i < maxDrones; i++) rotulosDosDrones[i].visible = false;
|
|
blocos.instanceMatrix.needsUpdate = true;
|
|
}
|
|
|
|
function sincronizarCaixas(p: Partida) {
|
|
const n = Math.min(p.caixas.length, MAX_CAIXAS);
|
|
caixas.count = n;
|
|
for (let i = 0; i < n; i++) {
|
|
const c = p.caixas[i];
|
|
peca.position.set(c.x, 0.22 + Math.sin(relogio * 3 + c.id) * 0.05, c.z);
|
|
peca.rotation.set(0, relogio * 1.5 + c.id, 0);
|
|
peca.scale.setScalar(1);
|
|
peca.updateMatrix();
|
|
caixas.setMatrixAt(i, peca.matrix);
|
|
}
|
|
peca.rotation.set(0, 0, 0);
|
|
caixas.instanceMatrix.needsUpdate = true;
|
|
}
|
|
|
|
return {
|
|
guardarPasso,
|
|
sincronizar(p, alfa, dt) {
|
|
relogio += dt;
|
|
const alturaDoTexto = alturaDoRotulo();
|
|
sincronizarZumbis(p, alfa, dt, alturaDoTexto);
|
|
sincronizarDrones(p, alfa, alturaDoTexto);
|
|
sincronizarCaixas(p);
|
|
},
|
|
descartar() {
|
|
for (const ator of reserva) {
|
|
ator.zumbi.descartar();
|
|
ator.rotulo.removeFromParent();
|
|
}
|
|
reserva.length = 0;
|
|
porId.clear();
|
|
cena.remove(blocos, caixas, ...rotulosDosDrones);
|
|
blocos.dispose();
|
|
caixas.dispose();
|
|
verde.dispose();
|
|
materialDaCaixa.dispose();
|
|
caixas.geometry.dispose();
|
|
modelo.bloco.dispose();
|
|
// a geometria dos sprites é uma só, do módulo do three: soltar libera o renderer desta partida
|
|
rotulosDosDrones[0].geometry.dispose();
|
|
for (const { material, textura } of rotulos.values()) {
|
|
material.dispose();
|
|
textura.dispose();
|
|
}
|
|
rotulos.clear();
|
|
for (const material of materiais.values()) material.dispose();
|
|
materiais.clear();
|
|
for (const textura of modelo.roupas) textura.dispose();
|
|
// o modelo fica em cache para a próxima partida; dispose só solta a GPU (e os ouvintes) deste renderer
|
|
modelo.zumbi.cena.traverse((objeto) => {
|
|
const malha = objeto as Mesh;
|
|
if (!malha.isMesh) return;
|
|
malha.geometry.dispose();
|
|
const material = malha.material as MeshStandardMaterial;
|
|
for (const valor of Object.values(material))
|
|
if ((valor as Texture | null)?.isTexture)
|
|
(valor as Texture).dispose();
|
|
material.dispose();
|
|
});
|
|
},
|
|
};
|
|
}
|