#!/usr/bin/env python3 """Mudancas do Motumbo para o fredygames (ver build-motumbo.sh). Uso: python3 -I patch.py . 1. Sem rede: o jogo online (matchmaking e salas por MQTT publico, WebRTC com STUN do Google, relay) sai. JUGAR vira partida rapida contra bots, a SALA PRIVADA some, signal.ts nao tem brokers e net.ts nao cria RTCPeerConnection; os links #room=... nao fazem nada. 2. Interface traduzida do espanhol (rioplatense) para o ingles: index.html, ui.ts, main.ts, sim.ts (arenas, modos, orbes), skins.ts, unlocks.ts, stats.ts, editor.ts, mapcodec.ts, render.ts, tune.ts. Os textos do jogo online (so alcancaveis com a rede) ficam como estao. 3. CSP que bloqueia qualquer requisicao fora da propria pagina. """ import re import sys from pathlib import Path root = Path(sys.argv[1]) web = root / 'web' src = web / 'src' def edit(path, *pairs): text = path.read_text(encoding='utf-8') for old, new in pairs: n = text.count(old) if n != 1: sys.exit(f'patch.py: {path.name}: esperava 1x {old[:70]!r}, achei {n}x') text = text.replace(old, new) path.write_text(text, encoding='utf-8') def edit_all(path, *pairs): """Como edit, mas cada par pode ocorrer varias vezes (pelo menos uma).""" text = path.read_text(encoding='utf-8') for old, new in pairs: if text.count(old) < 1: sys.exit(f'patch.py: {path.name}: nao achei {old[:70]!r}') text = text.replace(old, new) path.write_text(text, encoding='utf-8') # ----------------------------------------------------------------------------- sem rede edit(src / 'signal.ts', ("const BROKERS = ['wss://broker.emqx.io:8084/mqtt', 'wss://test.mosquitto.org:8081'];", "const BROKERS: string[] = []; // Fredygames: sem jogo online, nenhum broker"), (" ws = new WebSocket(this.url, 'mqtt');", " throw new Error('Online play is not available here.'); // Fredygames: sem rede")) edit(src / 'net.ts', ("this.pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });", "throw new Error('Online play is not available here.'); // Fredygames: sem rede")) # JUGAR = partida rapida contra bots, sem procurar rival; sem sala privada; sem links #room= text = (src / 'main.ts').read_text(encoding='utf-8') a = text.index(' // Slither-style quick match: hunt the lobby for a rival while you play.') b = text.index(" ui.toast('sin rival aún — jugás con bots (seguimos buscando)');\n };\n") + len(" ui.toast('sin rival aún — jugás con bots (seguimos buscando)');\n };\n") new_quick = ''' // Fredygames: quick match = a game against bots (there is no online play here). async function startQuickMatch(): Promise { teardownOnline(); audio.uiClick(); quickMatching = false; quickMatchFellBack = false; quickBotsRunning = false; intent = 'solo'; levelChoice = 'random'; gameMode = MODE_SUMO; gameModeParam = 0; winTarget = 5; botDifficulty = 1; startMatch(); } ''' text = text[:a] + new_quick + text[b:] (src / 'main.ts').write_text(text, encoding='utf-8') edit(src / 'main.ts', (" const roomLink = location.hash.match(/^#room=([A-Za-z0-9-]{4,12})/i);", " const roomLink: RegExpMatchArray | null = null; // Fredygames: sin salas online")) edit(src / 'ui.ts', (""" this.lastRoomAction = { type: 'quick' }; this.showAny('online'); this.setRoomState('searching', 'buscando jugadores…'); this.cb.onQuickMatch();""", """ this.cb.onQuickMatch(); // Fredygames: partida contra bots, sem tela de busca online""")) # ----------------------------------------------------------------------------- CSP + index.html csp = ("default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; " "media-src 'self' blob:; connect-src 'self' data: blob:; worker-src 'self' blob:; font-src 'self'; frame-src 'none'; " "form-action 'none'; base-uri 'none'") html = web / 'index.html' text = html.read_text(encoding='utf-8') text = re.sub(r'', '', text, count=1) text = re.sub(r'', '', text, count=1) text = text.replace('', '', 1) text = text.replace('', '\n ', 1) html.write_text(text, encoding='utf-8') edit(html, ('MOTUMBO — sumo físico multijugador en el navegador', 'MOTUMBO — physics sumo in the browser'), ('content="MOTUMBO — sumo físico multijugador"', 'content="MOTUMBO — physics sumo"'), ('
esperando al rival…
', '
waiting for the rival…
'), ('SUMO · FÍSICA · PLATAFORMA QUE SE CAE', 'SUMO · PHYSICS · CRUMBLING PLATFORM'), ('placeholder="tu nombre"', 'placeholder="your name"'), ('TU BOLA', 'YOUR BALL'), ('JUGARTE EMPAREJAMOS ONLINE · SI NO HAY NADIE, BOTS', 'PLAYQUICK MATCH AGAINST BOTS'), ('PARTIDA PERSONALIZADAELEGÍ ARENA, MODO Y RIVALES', 'CUSTOM GAMEPICK THE ARENA, MODE AND RIVALS'), ('', ''), ('EDITORCREÁ TU ARENA', 'EDITORBUILD YOUR OWN ARENA'), ('', ''), ('', ''), ('

PARTIDA

', '

MATCH

'), ('
RIVALES
', '
RIVALS
'), ('title="Cuántas bolas en la arena (vos + bots)"', 'title="How many balls in the arena (you + bots)"'), ('AMIGO', 'FRIEND'), ('en el mismo teclado', 'on the same keyboard'), ('
BOTS EN LA SALA
', '
BOTS IN THE ROOM
'), ('', ''), ('ALEATORIO', 'RANDOM'), ('
ARENA
', '
ARENA
'), ('
MODO
', '
MODE
'), ('data-mode="1">REY', 'data-mode="1">KING'), ('data-mode="2">COSECHA', 'data-mode="2">HARVEST'), ('data-mode="3">MALDITO', 'data-mode="3">CURSED'), ('RONDAS', 'ROUNDS'), ('', ''), ('

CÓMO JUGAR

', '

HOW TO PLAY

'), ('Moverte. La bola tiene inercia: frená antes del borde.', 'Move. The ball has inertia: brake before the edge.'), ('Dash — golpear con dash empuja mucho más fuerte.', 'Dash — hitting with a dash pushes much harder.'), ('EspacioSalto.', 'SpaceJump.'), ('Ancla — frená en seco. Justo antes de un dash rival = parry.', 'Anchor — stop dead. Just before a rival\'s dash = parry.'), ('El orbe dorado carga tu próximo dash ×2.3.', 'The golden orb charges your next dash ×2.3.'), ('Pausa / revancha.', 'Pause / rematch.'), ('

Gana el último en pie. Los otros modos se explican al elegirlos.

', '

The last one standing wins. The other modes are explained when you pick them.

'), ('

PAUSA

', '

PAUSED

'), ('', ''), ('', ''), ('', ''), ('

FIN DE RONDA

', '

ROUND OVER

'), ('', ''), ('', ''), ('

OPCIONES

', '

OPTIONS

'), ('Volumen general', 'Master volume'), ('Música', 'Music'), ('Sombras', 'Shadows'), ('Partículas', 'Particles'), ('', ''), ('', ''), ('', ''), ('Reducir sacudidasmenos screen-shake y animaciones', 'Reduce shakingless screen shake and fewer animations'), ('P1 WASD · Shift dash · Espacio salto · Ctrl ancla
', 'P1 WASD · Shift dash · Space jump · Ctrl anchor
'), ('P2 Flechas · Shift der. dash · Ctrl der. salto · . ancla
', 'P2 Arrows · Right Shift dash · Right Ctrl jump · . anchor
'), ('Esc pausa · R revancha\n ', 'Esc pause · R rematch\n '), ('

EDITOR DE MAPAS

', '

MAP EDITOR

'), ('title="Baldosa a nivel del piso">PISO', 'title="Tile at floor level">FLOOR'), ('title="Subí un piso (arrastrá o rueda del mouse) — hasta torres de 15 pisos">SUBIR ▲', 'title="Raise one level (drag or mouse wheel) — up to towers of 15 levels">RAISE ▲'), ('title="Bajá un piso; en el suelo saca la baldosa">BAJAR ▼', 'title="Lower one level; on the ground it removes the tile">LOWER ▼'), ('title="Sacar baldosa (también con click derecho)">BORRAR', 'title="Remove tile (also with right click)">ERASE'), ('title="Punto de aparición (máx 12, click para poner/sacar)">SPAWN', 'title="Spawn point (max 12, click to place/remove)">SPAWN'), ('Nombre', 'Name'), ('placeholder="MI ARENA"', 'placeholder="MY ARENA"'), ('Tema', 'Theme'), ('Empieza a derrumbarsesegundos desde el arranque', 'Starts crumblingseconds from the start'), ('Velocidad del derrumbe', 'Crumble speed'), ('Barra giratorialargo del brazo desde el centro', 'Spinning bararm length from the centre'), ('', ''), ('', ''), ('', ''), ('', '')) # os varios "Volver" e "FÁCIL/NORMAL/DIFÍCIL" edit_all(html, ('← Volver', '← Back'), ('>FÁCIL', '>EASY'), ('>DIFÍCIL', '>HARD'), ('>BOTS', '>BOTS')) # ----------------------------------------------------------------------------- sim.ts sim = src / 'sim.ts' text = sim.read_text(encoding='utf-8') pairs = [("'CLÁSICA'", "'CLASSIC'"), ("'ANILLO'", "'RING'"), ("'PUENTES',\n 'RULETA'", "'BRIDGES',\n 'ROULETTE'"), ("'PIRÁMIDE'", "'PYRAMID'"), ("'HERRADURA'", "'HORSESHOE'"), ("'PASARELA'", "'CATWALK'"), ("'TARIMAS'", "'PLATFORMS'"), ("'CRUZ',\n", "'CROSS',\n"), ("'ASPAS'", "'BLADES'"), ("'GEMELAS'", "'TWINS'"), ("'PANAL'", "'HONEYCOMB'"), ("'DIANA'", "'BULLSEYE'"), ("'VOLCÁN'", "'VOLCANO'"), ("'ZIGURAT'", "'ZIGGURAT'"), ("'TORRES',\n 'RULETA DOBLE'", "'TOWERS',\n 'DOUBLE ROULETTE'"), ("'FÁBRICA'", "'FACTORY'"), ("'MARTILLO'", "'HAMMER'"), ("'CALLES'", "'STREETS'"), ("['NOVA', 'CRÁTER', 'ARRECIFE', 'FALLA', 'TÚNEL', 'BASTIÓN', 'PLAZA', 'ISLOTE', 'CIRCUITO', 'MESETA']", "['NOVA', 'CRATER', 'REEF', 'FAULT', 'TUNNEL', 'BASTION', 'PLAZA', 'ISLET', 'CIRCUIT', 'MESA']"), ("'⊕ COLOSO', '⊕ ARCHIPIÉLAGO', '⊕ GRAN CRUZ', '⊕ DOBLE ANILLO', '⊕ ESTADIO', '⊕ TORRE',", "'⊕ COLOSSUS', '⊕ ARCHIPELAGO', '⊕ GRAND CROSS', '⊕ DOUBLE RING', '⊕ STADIUM', '⊕ TOWER',"), ("'⊕ SALTOS', '⊕ CRÁTER', '⊕ PUENTES ALTOS', '⊕ ISLOTES', '⊕ TORRE ALTA',", "'⊕ JUMPS', '⊕ CRATER', '⊕ HIGH BRIDGES', '⊕ ISLETS', '⊕ HIGH TOWER',"), ("name: 'SÚPER', desc: 'próximo dash ×2.3'", "name: 'SUPER', desc: 'next dash ×2.3'"), ("desc: 'más velocidad (acumulable)'", "desc: 'more speed (stacks)'"), ("desc: 'crecés y pesás más'", "desc: 'you grow and get heavier'"), ("name: 'ESCUDO', desc: 'bloquea el próximo empujón'", "name: 'SHIELD', desc: 'blocks the next shove'"), ("name: 'BOMBA', desc: 'onda que empuja a todos'", "name: 'BOMB', desc: 'a shockwave that pushes everyone'"), ("name: 'IMÁN', desc: 'atrae a todos hacia vos'", "name: 'MAGNET', desc: 'pulls everyone toward you'"), ("name: 'CONGELAR', desc: 'congela al rival más cercano'", "name: 'FREEZE', desc: 'freezes the nearest rival'"), ("name: 'RESORTE', desc: 'súper salto — ideal en las torres'", "name: 'SPRING', desc: 'super jump — great on the towers'"), ("['SUMO', 'REY DE LA COLINA', 'COSECHA', 'MALDITO']", "['SUMO', 'KING OF THE HILL', 'HARVEST', 'CURSED']")] for old, new in pairs: if text.count(old) != 1: sys.exit(f'patch.py: sim.ts: esperava 1x {old[:60]!r}, achei {text.count(old)}x') text = text.replace(old, new) sim.write_text(text, encoding='utf-8') # ----------------------------------------------------------------------------- skins.ts skins = src / 'skins.ts' text = skins.read_text(encoding='utf-8') names = {'Brasil': 'Brazil', 'España': 'Spain', 'Italia': 'Italy', 'Francia': 'France', 'Alemania': 'Germany', 'Japón': 'Japan', 'México': 'Mexico', 'Japón Sol': 'Rising Sun', 'Grecia': 'Greece', 'Perú': 'Peru', 'Suecia': 'Sweden', 'Irlanda': 'Ireland', 'Rayas': 'Stripes', 'Lunares': 'Dots', 'Bandas': 'Bands', 'Damero': 'Checkers', 'Onda': 'Wave', 'Aros': 'Rings', 'Triángulos': 'Triangles', 'Estrellas': 'Stars', 'Camuflaje': 'Camouflage', 'Corazones': 'Hearts', 'Cebra': 'Zebra', 'Cuadros': 'Squares', 'Rombos': 'Diamonds', 'Peluda': 'Furry', 'Neón': 'Neon', 'Piedra': 'Stone', 'Cromo': 'Chrome', 'Galaxia': 'Galaxy', 'Oro': 'Gold', 'Cobre': 'Copper', 'Hielo': 'Ice', 'Arcoíris': 'Rainbow', 'Circuito': 'Circuit'} for es, en in names.items(): for pat in (f"{{ name: '{es}',", f" ['{es}', "): text = text.replace(pat, pat.replace(es, en)) skins.write_text(text, encoding='utf-8') # ----------------------------------------------------------------------------- unlocks.ts, stats.ts edit(src / 'unlocks.ts', ("matches: (n) => `Jugá ${n} ${n === 1 ? 'partida' : 'partidas'}`,", "matches: (n) => `Play ${n} ${n === 1 ? 'match' : 'matches'}`,"), ("matchWins: (n) => `Ganá ${n} ${n === 1 ? 'partida entera' : 'partidas enteras'}`,", "matchWins: (n) => `Win ${n} full ${n === 1 ? 'match' : 'matches'}`,"), ("rounds: (n) => `Jugá ${n} rondas`,", "rounds: (n) => `Play ${n} rounds`,"), ("roundWins: (n) => `Ganá ${n} ${n === 1 ? 'ronda' : 'rondas'}`,", "roundWins: (n) => `Win ${n} ${n === 1 ? 'round' : 'rounds'}`,"), ("bestStreak: (n) => `Hacé una racha de ${n} rondas`,", "bestStreak: (n) => `Win ${n} rounds in a row`,")) edit(src / 'stats.ts', ("return 'Todavía no jugaste ningún match — dale, tumbá a alguien.';", "return \"You haven't played a match yet — go knock somebody off.\";"), ("const parts = [`${s.matches} ${s.matches === 1 ? 'match' : 'matches'} (${s.matchWins} ganados)`, `${s.rounds} rondas (${s.roundWins} ganadas)`];", "const parts = [`${s.matches} ${s.matches === 1 ? 'match' : 'matches'} (${s.matchWins} won)`, `${s.rounds} rounds (${s.roundWins} won)`];"), ("parts.push(`mejor racha ${s.bestStreak}`);", "parts.push(`best streak ${s.bestStreak}`);")) # ----------------------------------------------------------------------------- ui.ts edit(src / 'ui.ts', ("{ label: 'ZONA', suffix", "{ label: 'ZONE', suffix"), ("{ label: 'ORBES', suffix", "{ label: 'ORBS', suffix"), ("{ label: 'MECHA', suffix", "{ label: 'FUSE', suffix"), ("'El último en pie gana la ronda.'", "'The last one standing wins the round.'"), ("'Controlá la zona (empujá a los demás afuera) y sumá segundos.'", "'Control the zone (push the others out) and rack up seconds.'"), ("'Juntá los orbes antes que nadie.'", "'Collect the orbs before anyone else.'"), ("'Pasale la maldición a otro antes de que explote.'", "'Pass the curse to someone else before it explodes.'"), ("del.title = 'Borrar';", "del.title = 'Delete';"), ('window.confirm(`¿Borrar "${m.name}"?`)', 'window.confirm(`Delete "${m.name}"?`)'), ("this.toast('Mapa borrado');", "this.toast('Map deleted');"), ("tag.textContent = 'VOS';", "tag.textContent = 'YOU';"), ("title.textContent = isFinal ? '🏆 ¡GANASTE! 🏆' : '¡GANASTE LA RONDA!';", "title.textContent = isFinal ? '🏆 YOU WIN! 🏆' : 'YOU WON THE ROUND!';"), ("title.textContent = isFinal ? 'PERDISTE' : 'CAÍSTE';", "title.textContent = isFinal ? 'YOU LOST' : 'YOU FELL';"), ("title.textContent = isFinal ? 'FIN DEL MATCH' : 'FIN DE RONDA';", "title.textContent = isFinal ? 'MATCH OVER' : 'ROUND OVER';"), ("sub.textContent = 'CAMPEÓN';", "sub.textContent = 'CHAMPION';"), ("bar('VIVO',", "bar('ALIVE',"), ("bar('EMPUJONES',", "bar('SHOVES',"), ("nextEl.textContent = 'siguiente ronda…';", "nextEl.textContent = 'next round…';"), ("nextEl.textContent = `siguiente ronda en ${left}…`;", "nextEl.textContent = `next round in ${left}…`;"), ("this.toast(`Bloqueada · ${unlockHint(i)}`);", "this.toast(`Locked · ${unlockHint(i)}`);"), ("let text = `${unlockedCount()}/${SKIN_COUNT} skins desbloqueadas`;", "let text = `${unlockedCount()}/${SKIN_COUNT} skins unlocked`;"), ("text += ` · próxima: ${next.hint}`;", "text += ` · next: ${next.hint}`;")) # ----------------------------------------------------------------------------- editor.ts, mapcodec.ts, render.ts, tune.ts edit(src / 'editor.ts', ("`TEMA ${i + 1}`", "`THEME ${i + 1}`"), ("this.toast('No se pudo cargar el mapa');", "this.toast('The map could not be loaded');"), ("|| 'SIN NOMBRE',", "|| 'UNNAMED',"), ("this.toast('Dibujá al menos una baldosa antes de exportar');", "this.toast('Draw at least one tile before exporting');"), ("window.prompt('Copiá el código del mapa:', code);", "window.prompt('Copy the map code:', code);"), ("() => this.toast('Código copiado al portapapeles'),", "() => this.toast('Code copied to the clipboard'),"), ("window.prompt('Pegá el código del mapa:');", "window.prompt('Paste the map code:');"), ("this.toast('Código inválido');", "this.toast('Invalid code');"), ("'IMPORTADO'", "'IMPORTED'"), ("this.toast('Mapa importado');", "this.toast('Map imported');"), ("window.confirm('¿Salir sin guardar? Se pierden los cambios.')", "window.confirm('Leave without saving? Your changes will be lost.')"), ("this.toast('El spawn necesita una baldosa debajo');", "this.toast('A spawn point needs a tile under it');"), ("this.toast(`Máximo ${MAX_SPAWNS} puntos de aparición`);", "this.toast(`At most ${MAX_SPAWNS} spawn points`);"), ("baldosas · ${this.spawns.length}/${MAX_SPAWNS} spawns`", "tiles · ${this.spawns.length}/${MAX_SPAWNS} spawns`"), ("this.toast('Mapa guardado');", "this.toast('Map saved');"), ("return v < 34 ? 'lento' : v < 67 ? 'medio' : 'rápido';", "return v < 34 ? 'slow' : v < 67 ? 'medium' : 'fast';")) edit(src / 'mapcodec.ts', ("'El mapa necesita al menos 8 baldosas.'", "'The map needs at least 8 tiles.'"), ("`Máximo ${MAX_TILES} baldosas.`", "`At most ${MAX_TILES} tiles.`"), ("'Marcá al menos 2 puntos de aparición.'", "'Mark at least 2 spawn points.'"), ("'Hay un punto de aparición sobre el vacío.'", "'A spawn point is over the void.'")) edit(src / 'render.ts', ("(label || 'VOS')", "(label || 'YOU')"), ("makeYouMarkerTexture('VOS')", "makeYouMarkerTexture('YOU')"), ("['ISOMÉTRICA', 'DESDE ARRIBA', 'TERCERA PERSONA']", "['ISOMETRIC', 'TOP-DOWN', 'THIRD PERSON']"), ("Math.random() < 0.5 ? '¡AFUERA!' : '¡KO!'", "Math.random() < 0.5 ? 'OUT!' : 'KO!'")) edit(src / 'tune.ts', ("'Cám. iso · altura'", "'Iso cam · height'"), ("'Cám. iso · distancia'", "'Iso cam · distance'"), ("'Cám. arriba · altura'", "'Top cam · height'"), ("'3ra pers. · atrás'", "'3rd person · back'"), ("'3ra pers. · altura'", "'3rd person · height'"), ("'Cara · inclinación'", "'Face · tilt'"), ("'Ojos · tamaño'", "'Eyes · size'")) # ----------------------------------------------------------------------------- main.ts edit(src / 'main.ts', ("['ROJO', 'AZUL', 'AMARILLO', 'VERDE', 'VIOLETA', 'NARANJA', 'TURQUESA', 'ROSA']", "['RED', 'BLUE', 'YELLOW', 'GREEN', 'PURPLE', 'ORANGE', 'TEAL', 'PINK']"), ("(vos)`", "(you)`"), ("name: 'PRUEBA DEL EDITOR'", "name: 'EDITOR TEST'"), ("📷 cámara: ${renderer.cycleCamera()}", "📷 camera: ${renderer.cycleCamera()}"), ("TUNING — ` para cerrar", "TUNING — ` to close"), ("'P1 WASD · dash Shift · salto Espacio · anclarse Ctrl';", "'P1 WASD · dash Shift · jump Space · anchor Ctrl';"), ("const cam = 'C cámara';", "const cam = 'C camera';"), ("if (mode === 'net') return `${p1}  |  ${cam} · Esc pausa`;", "if (mode === 'net') return `${p1}  |  ${cam} · Esc pause`;"), ("if (intent === 'solo') return `${p1}  |  ${cam} · Esc pausa · R revancha`;", "if (intent === 'solo') return `${p1}  |  ${cam} · Esc pause · R rematch`;"), ("`${p1}  |  P2 Flechas · Shift der. · Ctrl der. · .` +", "`${p1}  |  P2 Arrows · Right Shift · Right Ctrl · .` +"), ("`  |  ${cam} · Esc pausa`\n );", "`  |  ${cam} · Esc pause`\n );"), ("ui.toast('⚠ ¡SE DERRUMBA!');", "ui.toast('⚠ THE FLOOR IS CRUMBLING!');"), ("ui.toast(`💀 ${PLAYER_NAMES[a]} arranca MALDITO`);", "ui.toast(`💀 ${PLAYER_NAMES[a]} starts CURSED`);"), ("ui.toast(`💀 la maldición pasó a ${PLAYER_NAMES[a]}`);", "ui.toast(`💀 the curse passed to ${PLAYER_NAMES[a]}`);"), ("countdownEl.textContent = '¡MOTUMBO!';", "countdownEl.textContent = 'MOTUMBO!';"))