Files
evandro 67b32fcab3
CI / audit (push) Successful in 10m48s
CI / verify (push) Successful in 19m2s
CI / deploy (push) Successful in 2m14s
CI / e2e (push) Failing after 36m15s
feat(jogo): expediente dos mortos, jogo de zumbis em primeira pessoa
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.
2026-11-10 19:37:10 -03:00

356 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Mapa do escritório do jogo em /jogo: gera a primeira versão do .tmx do Tiled e converte o JSON exportado para a grade da lógica.
A fonte do mapa é o 3d/mapa/escritorio.tmx, editado à mão no Tiled. Rodar da raiz do repositório (o 3d/mapa.sh faz a cadeia toda):
python3 3d/mapa.py gerar sobrescreve 3d/mapa/escritorio.tmx e 3d/mapa/ladrilhos.png pela semente, apagando as edições
feitas no Tiled; só roda sob `bash 3d/mapa.sh --gerar`
python3 3d/mapa.py converter lê 3d/mapa/escritorio.json (exportado pelo Tiled) e escreve lib/jogo/mapas/escritorio.ts
Coordenadas em células de 1 m: x cresce para a direita (leste), z para baixo (sul) no desenho; retângulos inclusivos.
"""
import json
import random
import struct
import sys
import zlib
from pathlib import Path
from xml.sax.saxutils import escape, quoteattr
SEMENTE = 20261110
LARGURA, ALTURA, LADO = 44, 32, 32
RAIZ = Path(__file__).resolve().parent.parent
PASTA = RAIZ / "3d" / "mapa"
SAIDA_TS = RAIZ / "lib" / "jogo" / "mapas" / "escritorio.ts"
# nome, cor RGB, grade da lógica ("#" bloqueia passo e visão, "M" só o passo, "" é enfeite), cobre a célula inteira
LADRILHOS = [
("parede", (55, 58, 70), "#", True),
("chão", (214, 206, 188), ".", True),
("mesa", (176, 120, 64), "M", True),
("cadeira", (40, 110, 200), "", False),
("armário", (110, 140, 110), "#", True),
("estante de servidor", (0, 150, 140), "#", True),
("sofá", (190, 50, 60), "M", True),
("bancada", (230, 180, 40), "M", True),
("planta de vaso", (60, 160, 60), "", False),
("bebedouro", (120, 200, 240), "", False),
]
GID = {nome: i + 1 for i, (nome, *_resto) in enumerate(LADRILHOS)}
PAREDES = [
# borda
(0, 0, 43, 0), (0, 31, 43, 31), (0, 0, 0, 31), (43, 0, 43, 31),
# faixas de salas e anel do corredor
(0, 7, 43, 7), (0, 24, 43, 24), (9, 7, 9, 24), (34, 7, 34, 24),
(12, 10, 31, 10), (12, 21, 31, 21), (12, 10, 12, 21), (31, 10, 31, 21),
# divisórias
(15, 0, 15, 7), (28, 0, 28, 7), (0, 16, 9, 16), (34, 16, 43, 16), (17, 24, 17, 31), (24, 24, 24, 31),
# poços dos elevadores, caixa da escada, cabines dos banheiros
(1, 29, 3, 30), (6, 29, 8, 30), (21, 27, 23, 30), (1, 17, 2, 21),
]
AMBIENTES = [
("recepção", 1, 1, 14, 6),
("sala de reunião", 16, 1, 27, 6),
("diretoria", 29, 1, 42, 6),
("copa", 1, 8, 8, 15),
("banheiros", 1, 17, 8, 23),
("área de mesas", 13, 11, 30, 20),
("sala do servidor", 35, 8, 42, 15),
("almoxarifado", 35, 17, 42, 23),
("hall dos elevadores", 1, 25, 16, 30),
("escada", 18, 25, 23, 30),
("área de serviço", 25, 25, 42, 30),
]
# trechos do anel, só para espalhar luminárias
CORREDOR = [(10, 8, 33, 9), (10, 22, 33, 23), (10, 10, 11, 21), (32, 10, 33, 21)]
PORTAS = [
("recepção – copa", 4, 7, 5, 7),
("recepção – corredor", 12, 7, 13, 7),
("sala de reunião – corredor", 18, 7, 19, 7),
("sala de reunião – corredor", 24, 7, 25, 7),
("sala de reunião – diretoria", 28, 3, 28, 4),
("diretoria – corredor", 30, 7, 31, 7),
("copa – corredor", 9, 11, 9, 12),
("banheiros – corredor", 9, 19, 9, 20),
("banheiros – hall dos elevadores", 4, 24, 5, 24),
("hall dos elevadores – corredor", 13, 24, 14, 24),
("hall dos elevadores – escada", 17, 27, 17, 28),
("escada – corredor", 20, 24, 21, 24),
("área de serviço – corredor", 28, 24, 29, 24),
("área de serviço – almoxarifado", 38, 24, 39, 24),
("almoxarifado – corredor", 34, 19, 34, 20),
("sala do servidor – almoxarifado", 38, 16, 39, 16),
("sala do servidor – corredor", 34, 11, 34, 12),
("área de mesas – corredor norte", 21, 10, 22, 10),
("área de mesas – corredor norte", 28, 10, 29, 10),
("área de mesas – corredor sul", 21, 21, 22, 21),
("área de mesas – corredor oeste", 12, 15, 12, 16),
("área de mesas – corredor leste", 31, 15, 31, 16),
]
ILHAS = [(15, 13, 19, 14), (23, 13, 27, 14), (15, 17, 19, 18), (23, 17, 27, 18)]
MOVEIS = [
# recepção
("sofá", 3, 1, 5, 1), ("sofá", 1, 3, 1, 6), ("bancada", 8, 3, 11, 3),
("cadeira", 9, 2, 9, 2), ("cadeira", 10, 2, 10, 2), ("planta de vaso", 1, 1, 1, 1), ("bebedouro", 14, 1, 14, 1),
# sala de reunião
("mesa", 18, 3, 25, 4), ("cadeira", 17, 3, 17, 3), ("cadeira", 26, 4, 26, 4),
("planta de vaso", 16, 1, 16, 1), ("planta de vaso", 27, 1, 27, 1),
# diretoria
("mesa", 35, 3, 38, 4), ("cadeira", 37, 2, 37, 2), ("cadeira", 36, 5, 36, 5), ("cadeira", 38, 5, 38, 5),
("sofá", 29, 1, 31, 1), ("armário", 42, 1, 42, 4), ("planta de vaso", 41, 6, 41, 6),
# copa
("bancada", 1, 11, 1, 14), ("bancada", 1, 15, 4, 15), ("armário", 7, 15, 8, 15), ("mesa", 4, 11, 5, 12),
("cadeira", 3, 11, 3, 11), ("cadeira", 6, 12, 6, 12), ("cadeira", 4, 10, 4, 10), ("cadeira", 5, 13, 5, 13),
("bebedouro", 1, 8, 1, 8),
# banheiros
("bancada", 5, 17, 8, 17), ("planta de vaso", 8, 23, 8, 23),
# hall dos elevadores
("sofá", 11, 30, 14, 30), ("planta de vaso", 1, 25, 1, 25), ("bebedouro", 16, 25, 16, 25),
# área de serviço
("armário", 27, 30, 32, 30), ("bancada", 40, 27, 42, 28), ("bebedouro", 33, 25, 33, 25),
# almoxarifado
("armário", 42, 17, 42, 19), ("armário", 37, 20, 39, 20), ("armário", 35, 23, 36, 23),
# sala do servidor
("estante de servidor", 37, 10, 42, 10), ("estante de servidor", 37, 13, 42, 13),
# área de mesas
*[("mesa", *ilha) for ilha in ILHAS],
("armário", 30, 11, 30, 13), ("planta de vaso", 13, 11, 13, 11), ("planta de vaso", 13, 20, 13, 20),
("planta de vaso", 30, 20, 30, 20), ("bebedouro", 30, 14, 30, 14),
# corredor
("planta de vaso", 10, 8, 10, 8), ("planta de vaso", 33, 8, 33, 8),
("planta de vaso", 10, 23, 10, 23), ("planta de vaso", 33, 23, 33, 23),
]
INICIO = (7, 5)
NASCEDOUROS = [
("elevador 1", 2, 27), ("elevador 2", 7, 27), ("escada", 19, 29), ("banheiros", 6, 21),
("fundos, área de serviço", 26, 29), ("fundos, área de serviço", 41, 30), ("fundos, almoxarifado", 41, 22),
]
# célula de chão e para onde o cartaz olha; a parede fica do lado oposto
CARTAZES = [
(7, 1, "S"), (12, 1, "S"), (21, 1, "S"), (16, 4, "L"), (36, 1, "S"), (8, 14, "O"), (20, 9, "N"),
(10, 18, "L"), (33, 14, "O"), (25, 22, "S"), (20, 11, "S"), (13, 18, "L"), (11, 25, "S"),
]
FACES = {"N": (0, 1), "S": (0, -1), "L": (-1, 0), "O": (1, 0)} # deslocamento até a parede
NOTAS = """Escritório do jogo em /jogo, 1 célula = 1 m. Este arquivo é a fonte do mapa: abra no Tiled (`flatpak run org.mapeditor.Tiled`), edite, salve e rode `bash 3d/mapa.sh`, que exporta o JSON, converte a grade da lógica e monta o 3D. `bash 3d/mapa.sh --gerar` refaz este arquivo pelo 3d/mapa.py e apaga as edições feitas aqui.
Camadas: planta (parede e chão), moveis (a propriedade grade de cada ladrilho diz como entra na lógica: # bloqueia passo e visão, M bloqueia o passo e deixa o tiro passar, vazio é só enfeite), marcas (objetos pelo tipo).
Tipos em marcas: inicio (ponto do jogador), nascedouro (ponto; o nome diz o lugar), ambiente (retângulo com o nome da sala), porta (retângulo sobre as células abertas na parede), luminaria (ponto no teto), cartaz (ponto na face da parede; face = para onde o cartaz olha: N para cima no desenho, S para baixo, L para a direita, O para a esquerda; imagem de 1 a 4)."""
def celulas(x0, z0, x1, z1):
return [(x, z) for z in range(z0, z1 + 1) for x in range(x0, x1 + 1)]
def png(largura, altura, rgba):
"""PNG RGBA 8 bits sem metadados (os mesmos pixels dão os mesmos bytes)."""
linhas = b"".join(b"\0" + rgba[y * largura * 4 : (y + 1) * largura * 4] for y in range(altura))
def bloco(tipo, dados):
return struct.pack(">I", len(dados)) + tipo + dados + struct.pack(">I", zlib.crc32(tipo + dados))
cabecalho = struct.pack(">IIBBBBB", largura, altura, 8, 6, 0, 0, 0)
return b"\x89PNG\r\n\x1a\n" + bloco(b"IHDR", cabecalho) + bloco(b"IDAT", zlib.compress(linhas, 9)) + bloco(b"IEND", b"")
def imagem_ladrilhos():
"""Uma linha de ladrilhos de 32 px: cheios com borda escura; enfeites são um quadrado menor sobre fundo transparente."""
largura = LADO * len(LADRILHOS)
pixels = bytearray(largura * LADO * 4)
for i, (_nome, cor, _grade, cheio) in enumerate(LADRILHOS):
escura = tuple(c // 2 for c in cor)
margem = 0 if cheio else 8
for y in range(margem, LADO - margem):
for x in range(margem, LADO - margem):
borda = min(x - margem, y - margem, LADO - 1 - margem - x, LADO - 1 - margem - y) < 2
p = (y * largura + i * LADO + x) * 4
pixels[p : p + 4] = bytes((*(escura if borda else cor), 255))
return png(largura, LADO, bytes(pixels))
def montar_planta():
planta = [["chão"] * LARGURA for _ in range(ALTURA)]
for parede in PAREDES:
for x, z in celulas(*parede):
planta[z][x] = "parede"
for _nome, *porta in PORTAS:
for x, z in celulas(*porta):
planta[z][x] = "chão"
return planta
def montar_moveis(rng):
moveis = [[None] * LARGURA for _ in range(ALTURA)]
for tipo, *retangulo in MOVEIS:
for x, z in celulas(*retangulo):
moveis[z][x] = tipo
# cadeiras dos dois lados de cada ilha; algumas vagas ficam sem cadeira
for x0, z0, x1, z1 in ILHAS:
for x in range(x0, x1 + 1):
for z in (z0 - 1, z1 + 1):
if rng.random() < 0.8:
moveis[z][x] = "cadeira"
return moveis
def objeto(id_, tipo, nome, x, y, largura=None, altura=None, propriedades=()):
attrs = f'id="{id_}" name={quoteattr(nome)} type="{tipo}" x="{x:g}" y="{y:g}"'
if largura is not None:
attrs += f' width="{largura:g}" height="{altura:g}"'
corpo = []
if propriedades:
corpo.append(" <properties>")
corpo += [f' <property name="{n}" {t}value={quoteattr(str(v))}/>' for n, t, v in propriedades]
corpo.append(" </properties>")
if largura is None:
corpo.append(" <point/>")
return [f" <object {attrs}>", *corpo, " </object>"]
def luminarias(x0, z0, x1, z1):
"""Pontos a cada ~5 m dentro do retângulo, em pixels, arredondados à meia célula."""
largura, altura = x1 - x0 + 1, z1 - z0 + 1
nx, nz = max(1, round(largura / 5)), max(1, round(altura / 5))
return [
(round((x0 + largura * (i + 0.5) / nx) * 2) * LADO // 2, round((z0 + altura * (j + 0.5) / nz) * 2) * LADO // 2)
for j in range(nz)
for i in range(nx)
]
def gerar():
rng = random.Random(SEMENTE)
planta = montar_planta()
moveis = montar_moveis(rng)
marcas = []
total = 0
def novo(*args, **kwargs):
nonlocal total
total += 1
marcas.extend(objeto(total, *args, **kwargs))
novo("inicio", "inicio", (INICIO[0] + 0.5) * LADO, (INICIO[1] + 0.5) * LADO)
for nome, x, z in NASCEDOUROS:
novo("nascedouro", nome, (x + 0.5) * LADO, (z + 0.5) * LADO)
for nome, x0, z0, x1, z1 in AMBIENTES:
novo("ambiente", nome, x0 * LADO, z0 * LADO, (x1 - x0 + 1) * LADO, (z1 - z0 + 1) * LADO)
for nome, x0, z0, x1, z1 in PORTAS:
novo("porta", nome, x0 * LADO, z0 * LADO, (x1 - x0 + 1) * LADO, (z1 - z0 + 1) * LADO)
for retangulo in [r[1:] for r in AMBIENTES] + CORREDOR:
for x, y in luminarias(*retangulo):
novo("luminaria", "luminaria", x, y)
for x, z, face in CARTAZES:
dx, dz = FACES[face]
assert planta[z][x] == "chão" and planta[z + dz][x + dx] == "parede", (x, z, face)
px = (x + 0.5 + dx / 2) * LADO
pz = (z + 0.5 + dz / 2) * LADO
propriedades = [("face", "", face), ("imagem", 'type="int" ', rng.randint(1, 4))]
novo("cartaz", "cartaz", px, pz, propriedades=propriedades)
def csv(grade):
return ",\n".join(",".join(str(GID[c] if c else 0) for c in linha) for linha in grade)
tiles = []
for i, (nome, _cor, grade, _cheio) in enumerate(LADRILHOS):
tiles += [
f' <tile id="{i}">',
" <properties>",
f" <property name=\"nome\" value={quoteattr(nome)}/>",
f' <property name="grade" value="{grade}"/>',
" </properties>",
" </tile>",
]
marcas_txt = "\n".join(marcas)
tmx = "\n".join(
[
'<?xml version="1.0" encoding="UTF-8"?>',
f'<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="{LARGURA}" height="{ALTURA}" tilewidth="{LADO}" tileheight="{LADO}" infinite="0" nextlayerid="4" nextobjectid="{total + 1}">',
" <properties>",
f' <property name="notas">{escape(NOTAS)}</property>',
f' <property name="semente" type="int" value="{SEMENTE}"/>',
" </properties>",
f' <tileset firstgid="1" name="ladrilhos" tilewidth="{LADO}" tileheight="{LADO}" tilecount="{len(LADRILHOS)}" columns="{len(LADRILHOS)}">',
f' <image source="ladrilhos.png" width="{LADO * len(LADRILHOS)}" height="{LADO}"/>',
*tiles,
" </tileset>",
f' <layer id="1" name="planta" width="{LARGURA}" height="{ALTURA}">',
' <data encoding="csv">',
csv(planta),
"</data>",
" </layer>",
f' <layer id="2" name="moveis" width="{LARGURA}" height="{ALTURA}">',
' <data encoding="csv">',
csv(moveis),
"</data>",
" </layer>",
' <objectgroup id="3" name="marcas">',
marcas_txt,
" </objectgroup>",
"</map>",
"",
]
)
PASTA.mkdir(parents=True, exist_ok=True)
(PASTA / "escritorio.tmx").write_text(tmx, encoding="utf-8")
(PASTA / "ladrilhos.png").write_bytes(imagem_ladrilhos())
def converter():
dados = json.loads((PASTA / "escritorio.json").read_text(encoding="utf-8"))
largura, altura, lado = dados["width"], dados["height"], dados["tilewidth"]
grade_por_gid = {}
for tileset in dados["tilesets"]:
for tile in tileset.get("tiles", []):
propriedades = {p["name"]: p["value"] for p in tile.get("properties", [])}
grade_por_gid[tileset["firstgid"] + tile["id"]] = propriedades["grade"]
camadas = {c["name"]: c for c in dados["layers"]}
linhas = [["#"] * largura for _ in range(altura)]
for nome in ("planta", "moveis"):
for i, gid in enumerate(camadas[nome]["data"]):
grade = grade_por_gid.get(gid & 0x1FFFFFFF, "") # bits altos: ladrilho espelhado ou girado
if grade:
linhas[i // largura][i % largura] = grade
ambientes, portas = [], []
for obj in camadas["marcas"]["objects"]:
x, z = int(obj["x"] // lado), int(obj["y"] // lado)
if obj["type"] in ("inicio", "nascedouro"):
linhas[z][x] = "J" if obj["type"] == "inicio" else "N"
elif obj["type"] in ("ambiente", "porta"):
retangulo = {"x": x, "z": z, "largura": round(obj["width"] / lado), "altura": round(obj["height"] / lado)}
if obj["type"] == "ambiente":
ambientes.append({"nome": obj["name"], **retangulo})
else:
portas.append(retangulo)
def lista(itens):
return "[\n" + "".join(f" {json.dumps(i, ensure_ascii=False)},\n" for i in itens) + "]"
retangulo_ts = "x: number; z: number; largura: number; altura: number"
ts = f"""// Gerado por `bash 3d/mapa.sh` (`python3 3d/mapa.py converter`) a partir de 3d/mapa/escritorio.tmx: não edite à mão.
/** Planta do escritório para `lerMapa`: `#` parede ou móvel alto, `M` móvel baixo, `.` chão, `J` início, `N` nascedouro. */
export const ESCRITORIO: readonly string[] = {lista("".join(linha) for linha in linhas)};
/** Salas com nome, em células (x para a direita, z para baixo). */
export const AMBIENTES: readonly {{ nome: string; {retangulo_ts} }}[] = {lista(ambientes)};
/** Células abertas nas paredes, em células. */
export const PORTAS: readonly {{ {retangulo_ts} }}[] = {lista(portas)};
"""
SAIDA_TS.parent.mkdir(parents=True, exist_ok=True)
SAIDA_TS.write_text(ts, encoding="utf-8")
if __name__ == "__main__":
comandos = {"gerar": gerar, "converter": converter}
if len(sys.argv) != 2 or sys.argv[1] not in comandos:
sys.exit("uso: python3 3d/mapa.py gerar|converter")
comandos[sys.argv[1]]()