"""Mudancas do porte do Pocket Broomball para a web (rodado por build-pocket-broomball.sh na arvore do jogo, que ja vem sem game/assets/teams/, store-images/, store-pages/, metadata/, flatpak/ e ROADMAP.md: tudo isso mostra ou cita times reais). python3 -I patch.py Os 102 logos de times reais do jogo (LicenseRef-TEAM-ICONS: "no licence is available and all rights belong to the corresponding team") e os nomes de times e ligas reais nao fazem parte desta versao. Os times viram times ficticios com nome gerado, mesmas cores, forca, velocidade e preco de antes, e cada um ganha um escudo generico desenhado aqui (dominio publico). Tambem saem tudo que abre paginas fora do jogo (OS.shell_open, botoes de redes e de lojas), o botao de sair, as bandeiras de idioma (eram arquivos da pasta de logos) e os trechos dos creditos que falam dos logos. """ import math, os, re, struct, sys, zlib root = sys.argv[1] game = os.path.join(root, 'game') src = os.path.join(game, 'src') def rd(p): return open(p, encoding='utf-8').read() def wr(p, s): open(p, 'w', encoding='utf-8', newline='').write(s) def sub1(s, old, new, count=1): assert s.count(old) == count, (old, s.count(old)) return s.replace(old, new) # ---------------------------------------------------------------- escudos W = 200 def png(path, px): raw = b''.join(b'\x00' + px[y * W * 4:(y + 1) * W * 4] for y in range(W)) def chunk(t, d): c = struct.pack('>I', len(d)) + t + d return c + struct.pack('>I', zlib.crc32(t + d) & 0xffffffff) open(path, 'wb').write(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', W, W, 8, 6, 0, 0, 0)) + chunk(b'IDAT', zlib.compress(raw, 9)) + chunk(b'IEND', b'')) def rgb(h): h = h.lstrip('#') return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) def lum(c): return 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2] def cov(sd): return max(0.0, min(1.0, 0.5 - sd)) def star_sd(x, y, r): # distancia aproximada a uma estrela de 5 pontas centrada na origem, raio externo r a = math.atan2(y, x) + math.pi / 2 sector = (a % (2 * math.pi / 5)) - math.pi / 5 d = math.hypot(x, y) inner = r * 0.42 # reta entre o vertice externo e o interno px, py = d * math.cos(sector), d * abs(math.sin(sector)) ox, oy = r * math.cos(0), 0.0 ix, iy = inner * math.cos(math.pi / 5), inner * math.sin(math.pi / 5) ex, ey = ix - ox, iy - oy nl = math.hypot(ex, ey) return ((px - ox) * ey - (py - oy) * ex) / nl * -1 PATTERNS = [ lambda x, y: abs(y) - 22, # faixa horizontal lambda x, y: abs(x - y) / math.sqrt(2) - 20, # faixa diagonal lambda x, y: abs((y + 18) - abs(x) * 0.8) / 1.28 - 16, # chevron lambda x, y: star_sd(x, y + 4, 48), # estrela lambda x, y: -x, # metade vertical lambda x, y: min(abs(x), abs(y)) - 14, # cruz lambda x, y: (abs(x) + abs(y)) / math.sqrt(2) - 38, # losango lambda x, y: abs(abs(y) - 30) - 11 if abs(y) < 55 else 99, # duas faixas ] def crest(c1, c2, c3, k, path): c1, c2, c3 = rgb(c1), rgb(c2), rgb(c3) if abs(lum(c2) - lum(c1)) < 40: c2 = (250, 250, 250) if lum(c1) < 128 else (30, 30, 40) if abs(lum(c3) - lum(c2)) < 60: c3 = (250, 250, 250) if lum(c2) < 128 else (30, 30, 40) pat = PATTERNS[k % len(PATTERNS)] cx = cy = W / 2 R = W / 2 - 3 out = bytearray(W * W * 4) for j in range(W): for i in range(W): x, y = i + 0.5 - cx, j + 0.5 - cy d = math.hypot(x, y) if d > R + 1: continue a_out = cov(d - R) a_in = cov(d - (R - 12)) a_pat = min(a_in, cov(pat(x, y))) # c1 (anel) -> c2 (campo) -> c3 (desenho) r = c1[0] * (1 - a_in) + (c2[0] * (1 - a_pat) + c3[0] * a_pat) * a_in g = c1[1] * (1 - a_in) + (c2[1] * (1 - a_pat) + c3[1] * a_pat) * a_in b = c1[2] * (1 - a_in) + (c2[2] * (1 - a_pat) + c3[2] * a_pat) * a_in o = (j * W + i) * 4 out[o] = int(r + 0.5); out[o + 1] = int(g + 0.5); out[o + 2] = int(b + 0.5); out[o + 3] = int(a_out * 255 + 0.5) png(path, bytes(out)) # ---------------------------------------------------------------- times ADJ = ['Amber', 'Azure', 'Crimson', 'Jade', 'Violet', 'Silver', 'Golden', 'Scarlet', 'Teal', 'Indigo', 'Coral', 'Ivory', 'Cobalt', 'Emerald', 'Ruby', 'Onyx', 'Saffron', 'Cyan', 'Maroon', 'Olive', 'Lilac', 'Copper', 'Slate', 'Mint'] NOUN = ['Foxes', 'Owls', 'Comets', 'Lynxes', 'Otters', 'Herons', 'Badgers', 'Stags', 'Pandas', 'Geckos', 'Marmots', 'Pilots', 'Voyagers', 'Pioneers', 'Anchors', 'Lanterns', 'Sparks', 'Glaciers', 'Walruses', 'Seals', 'Narwhals', 'Puffins', 'Yetis', 'Beavers', 'Magpies', 'Bisons', 'Ibexes', 'Cranes', 'Kites', 'Orcas'] LEAGUES = { 'World Cup': 'Grand Cup', 'Italian Broomball League': 'Aurora League', 'Maritime Broomball Association': 'Harbor League', 'North American League': 'Summit League', 'Swiss Broomball Association': 'Glacier League', 'International League': 'Meadow League', 'International Women League': 'Lakeside League', 'Penrith Broomball Competition': 'Frost League', 'Ontario League': 'Ember League', } p = os.path.join(src, 'global/Teams.gd') s = rd(p) assert not os.path.exists(os.path.join(game, 'assets/teams')), 'game/assets/teams/ deveria ter saido do fonte' old_names = set(re.findall(r'^\t\t\t"name": "([^"]+)",$', s, re.M)) s = re.sub(r'\tvar (?!break_icon)\w+_icon = load\(\s*"[^"]*"\s*\)\n', '', s) s = re.sub(r'\t# [A-Z ]+\n', '', s) os.makedirs(os.path.join(game, 'assets/teams/generic')) names, shorts, k = set(), set(), 0 team_re = re.compile(r'(\t\t\{\n\t\t\t"colors": \["(#\w+)", "(#\w+)", "(#\w+)"\],\n)\t\t\t"short_name": "[^"]*",\n(\t\t\t"id": -?\d+,\n)\t\t\t"name": "[^"]*",\n(.*?)\t\t\t"icon": \w+,\n', re.S) def team(m): global k k += 1 n = k - 1 while True: name = f'{ADJ[n % len(ADJ)]} {NOUN[(n // len(ADJ) * 7 + n * 11) % len(NOUN)]}' if name not in names and name not in old_names: break n += 1 names.add(name) adj, noun = name.split() for cand in (adj[0] + noun[:2], adj[:2] + noun[0], adj[0] + noun[0] + noun[2], adj[:3]): if cand.upper() not in shorts: break else: raise SystemExit('sem sigla livre para ' + name) shorts.add(cand.upper()) icon = f'c{k:03d}.png' crest(m.group(2), m.group(3), m.group(4), k, os.path.join(game, 'assets/teams/generic', icon)) return (m.group(1) + f'\t\t\t"short_name": "{cand.upper()}",\n' + m.group(5) + f'\t\t\t"name": "{name}",\n' + m.group(6) + f'\t\t\t"icon": load("res://assets/teams/generic/{icon}"),\n') s = team_re.sub(team, s) assert k == 104, k # 92 times de ligas + 12 da copa (o id 77 aparece duas vezes nos dados do autor); o "Game break" e a parte assert '_icon' not in s.replace('"icon"', '').replace('break_icon', ''), 'sobrou referencia a icone' for old, new in LEAGUES.items(): s = sub1(s, f'"name": "{old}"', f'"name": "{new}"') wr(p, s) for bad in old_names: assert f'"{bad}"' not in s, bad p = os.path.join(src, 'ui/championship/team-picker/TeamPicker.gd') wr(p, sub1(rd(p), '== "World Cup"', '== "Grand Cup"')) p = os.path.join(src, 'global/Global.gd') wr(p, sub1(rd(p), '#unlock india and germany, if not unlocked yet', '#unlock the first two teams of the cup, if not unlocked yet')) # cenas: onde apontavam para um logo/bandeira (so como imagem padrao do editor), apontam para o primeiro escudo for d, _, fs in os.walk(src): for f in fs: if f.endswith('.tscn'): q = os.path.join(d, f) t = rd(q) u = re.sub(r'res://assets/teams/(?!generic/)[^"]+', 'res://assets/teams/generic/c001.png', t) if u != t: wr(q, u) # ---------------------------------------------------------------- cenas: tirar nos def remove_nodes(path, nodes, drop_resources=True): s = rd(path) blocks = re.split(r'\n(?=\[)', s) nodes = set(nodes) def full(head): name = re.search(r'name="([^"]+)"', head).group(1) m = re.search(r' parent="([^"]*)"', head) parent = m.group(1) if m else None if parent is None: return '' return name if parent == '.' else parent + '/' + name def gone(n): return any(n == x or n.startswith(x + '/') for x in nodes) keep, seen = [], set() for b in blocks: head = b.split('\n', 1)[0] if head.startswith('[node '): n = full(head) if gone(n): seen.add(n) continue elif head.startswith('[connection '): fr = re.search(r'from="([^"]*)"', head).group(1) to = re.search(r'to="([^"]*)"', head).group(1) if gone(fr) or gone(to): continue keep.append(b) assert nodes <= seen, nodes - seen s = '\n'.join(keep).rstrip('\n') + '\n' if drop_resources: out = [] for b in s.split('\n'): m = re.match(r'\[ext_resource .* id=(\d+)\]$', b) if m and not re.search(r'ExtResource\( %s \)' % m.group(1), s): continue out.append(b) s = '\n'.join(out) n = len(re.findall(r'^\[(ext|sub)_resource ', s, re.M)) + 1 s = re.sub(r'load_steps=\d+', f'load_steps={n}', s, count=1) wr(path, s) def remove_funcs(path, names): lines = rd(path).split('\n') for n in names: start = next((i for i, l in enumerate(lines) if l.startswith(f'func {n}(')), None) assert start is not None, n end = start + 1 while end < len(lines) and (lines[end] == '' or lines[end][0] in ' \t'): end += 1 del lines[start:end] wr(path, re.sub(r'\n{4,}', '\n\n\n', '\n'.join(lines)).rstrip('\n') + '\n') ui = os.path.join(src, 'ui') # menu: sem sair, sem site do autor e sem botoes de redes remove_nodes(os.path.join(ui, 'menu/MenuScreen.tscn'), ['VBoxContainer/Exit', 'VBoxContainer/HSeparator', 'VBoxContainer/SimonDalvai', 'VBoxContainer/HSeparator2', 'VBoxContainer/LinkButtons']) p = os.path.join(ui, 'menu/MenuScreen.gd') remove_funcs(p, ['_on_Exit_pressed', '_on_SimonDalvai_pressed', '_on_Codeberg_pressed', '_on_Mastodon_pressed', '_on_Github_pressed']) wr(p, sub1(rd(p), '\tif OS.get_name() == "iOS":\n\t\t$VBoxContainer/Exit.hide()\n\n', '')) # configuracoes: sem "mais jogos", sem "adicione seu time" e sem bandeira remove_nodes(os.path.join(ui, 'settings/Settings.tscn'), ['VBoxContainer/MoreGames', 'VBoxContainer/Join', 'VBoxContainer/Language/Flag']) p = os.path.join(ui, 'settings/Settings.gd') remove_funcs(p, ['_on_MoreGames_pressed', '_on_Join_pressed']) s = rd(p) s = re.sub(r'const FLAGS: Dictionary = \{.*?\n\}\n\n', '', s, flags=re.S) s = sub1(s, 'onready var flag: TextureRect = $VBoxContainer/Language/Flag\n', '') s = sub1(s, '\tflag.texture = load("res://assets/teams/national-teams/" + FLAGS[Global.locale])\n', '') wr(p, s) # info: sem botoes de site e de projeto, sem links; creditos sem os logos remove_nodes(os.path.join(ui, 'info/Info.tscn'), ['VBoxContainer/Buttons/Website', 'VBoxContainer/Buttons/Github', 'VBoxContainer/Buttons/Join'], drop_resources=False) p = os.path.join(ui, 'info/Info.gd') remove_funcs(p, ['_on_Website_pressed', '_on_Github_pressed', '_on_RichTextLabel_meta_clicked', '_on_Join_pressed']) q = os.path.join(ui, 'info/Info.tscn') t = rd(q) t = re.sub(r'\[connection signal="meta_clicked"[^\n]*\n', '', t) t = re.sub(r'(?<=[^\s\"])\[url=[^\]]*\]', ' ', t) t = re.sub(r'\[url=[^\]]*\]', '', t).replace('[/url]', '') t, n1 = re.subn(r'\\"Simple icons\\"\n[^\n]*\nLicense: CC0-1\.0\n\n', '', t) t, n2 = re.subn(r'Team logos\nThe team logos where send to me by themselves[^\n]*\n[^\n]*\nIf you want to use them[^\n]*\n\n', 'Team crests\nThe teams and their names are made up, and their crests were drawn for this web version by FredyGames and released to the public domain (CC0).\n\n', t) assert n1 >= 1 and n2 >= 1, (n1, n2) wr(q, t) # nenhum OS.shell_open ou OS.get_name de loja deve restar for d, _, fs in os.walk(src): for f in fs: if f.endswith('.gd'): assert 'shell_open' not in rd(os.path.join(d, f)), f # O importador de CSV do Godot 3 comprime as traducoes com um hash perfeito sorteado a cada importacao, o que muda o .pck a # cada build; sem compressao a saida e sempre a mesma (sao textos curtos). p = os.path.join(game, 'translations/translations.csv.import') wr(p, sub1(rd(p), 'compress=true', 'compress=false'))