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.
356 lines
16 KiB
Python
356 lines
16 KiB
Python
"""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]]()
|